Migrando do AWS PDK
Este guia orienta você através de um exemplo de migração de um projeto AWS PDK para o Nx Plugin for AWS, além de fornecer orientações gerais sobre este tópico.
Migrar para o Nx Plugin for AWS oferece os seguintes benefícios em relação ao PDK:
- Builds mais rápidos
- Mais fácil de usar (UI e CLI)
- Amigável para vibe-coding (experimente nosso servidor MCP!)
- Tecnologias mais modernas
- Desenvolvimento local de API e website
- Mais controle (modifique arquivos fornecidos para se adequar ao seu caso de uso)
- E muito mais!
Exemplo de Migração: Aplicação de Lista de Compras
Seção intitulada “Exemplo de Migração: Aplicação de Lista de Compras”Neste guia, usaremos a Aplicação de Lista de Compras do Tutorial do PDK como nosso projeto alvo para migrar. Siga as etapas desse tutorial para criar o projeto alvo se desejar acompanhar você mesmo.
A aplicação de lista de compras consiste nos seguintes tipos de projeto PDK:
MonorepoTsProjectTypeSafeApiProjectCloudscapeReactTsWebsiteProjectInfrastructureTsProject
Criar Workspace
Seção intitulada “Criar Workspace”Para começar, criaremos um novo workspace para nosso novo projeto. Embora seja mais extremo do que uma migração no local, essa abordagem nos dá o resultado final mais limpo. Criar um workspace Nx é equivalente a usar o MonorepoTsProject do 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=cdkAbra o diretório shopping-list que este comando cria no seu IDE favorito.
Migrar a API
Seção intitulada “Migrar a API”O TypeSafeApiProject usado na aplicação de lista de compras fez uso de:
- Smithy como linguagem de modelagem
- TypeScript para implementar operações
- Geração de hooks TypeScript para integração com um site react
Portanto, podemos usar o gerador ts#smithy-api para fornecer funcionalidade equivalente.
Gerar uma API Smithy TypeScript
Seção intitulada “Gerar uma API Smithy TypeScript”Execute o gerador ts#api com framework definido como smithy para configurar seu projeto de api em 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-interactiveVocê também pode realizar uma execução simulada para ver quais arquivos seriam alterados
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 o Nx Console VSCode Plugin se ainda não o fez
- Abra o console Nx no VSCode
- Clique em
Generate (UI)na seção "Common Nx Commands" - Procure por
@aws/nx-plugin - ts#api - Preencha os parâmetros obrigatórios
- name: api
- framework: smithy
- namespace: com.aws
- auth: iam
- Clique em
Generate
Você notará que isso gera um projeto model, bem como um projeto backend. O projeto model contém seu modelo Smithy, e backend contém sua implementação de servidor.
O backend usa o Smithy Server Generator for TypeScript. Exploraremos isso mais adiante abaixo.
Migrar o Modelo Smithy
Seção intitulada “Migrar o Modelo Smithy”Agora que temos a estrutura básica para nosso projeto de API Smithy, podemos migrar o modelo:
-
Exclua os arquivos Smithy de exemplo gerados em
packages/api/model/src -
Copie seu modelo do diretório
packages/api/model/src/main/smithydo projeto PDK para o diretóriopackages/api/model/srcdo seu novo projeto. -
Atualize o nome do serviço e o namespace em
smithy-build.jsonpara corresponder à aplicação PDK:smithy-build.json "plugins": {"openapi": {"service": "com.aws#MyApi",... -
Atualize o serviço em
main.smithypara adicionar o erroValidationException, que é necessário ao usar o Smithy TypeScript Server SDK.main.smithy use smithy.framework#ValidationException/// My Shopping List API@restJson1service MyApi {version: "1.0"operations: [GetShoppingListsPutShoppingListDeleteShoppingList]errors: [BadRequestErrorNotAuthorizedErrorInternalFailureErrorValidationException]} -
Adicione um arquivo
extensions.smithyempackages/api/model/srconde definiremos uma trait que fornece informações de paginação ao cliente gerado:extensions.smithy $version: "2"namespace com.awsuse smithy.openapi#specificationExtension@trait@specificationExtension(as: "x-cursor")structure cursor {inputToken: Stringenabled: Boolean} -
Adicione a nova trait
@cursorà operaçãoGetShoppingListsemget-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}Quaisquer operações
@paginatedtambém devem usar@cursorse você estiver usando o gerador de cliente fornecido pelo Nx Plugin for AWS (via o geradorapi-connection). -
Finalmente, remova a trait
@handlerde todas as operações, pois isso não é suportado pelo Nx Plugin for AWS. Usandots#smithy-api, não precisamos dos constructs CDK de função lambda gerados automaticamente e dos alvos de empacotamento gerados por essa trait, pois usamos um único pacote para todas as funções lambda.
Neste ponto, vamos executar uma compilação para verificar nossas alterações no modelo e garantir que temos algum código de servidor gerado para trabalhar. Haverá algumas falhas no projeto backend (@shopping-list/api), mas vamos resolver isso em seguida.
pnpm nx run-many --target buildyarn nx run-many --target buildnpx nx run-many --target buildbunx nx run-many --target buildMigrar os Handlers Lambda
Seção intitulada “Migrar os Handlers Lambda”Você pode considerar o projeto api/backend como algo equivalente ao projeto api/handlers/typescript do Type Safe API.
Uma das principais diferenças entre Type Safe API e o gerador ts#smithy-api é que os handlers são implementados usando o Smithy Server Generator for TypeScript, em vez dos próprios wrappers de handler gerados do Type Safe API (encontrados no projeto api/generated/typescript/runtime).
Os handlers lambda da aplicação de lista de compras dependem do pacote @aws-sdk/client-dynamodb, então vamos instalá-lo no projeto @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/apiEm seguida, vamos copiar o arquivo handlers/src/dynamo-client.ts do projeto PDK para backend/src/operations para que esteja disponível para nossos handlers.
O gerador ts#smithy-api cria um exemplo de operação Echo. Como removemos isso do nosso modelo, exclua o handler correspondente em backend/src/operations/echo.ts. Registraremos nossas operações migradas em service.ts mais adiante abaixo.
Para migrar os handlers, você pode seguir estas etapas gerais:
-
Copie o handler do diretório
packages/api/handlers/typescript/srcdo seu projeto PDK para o diretóriopackages/api/backend/src/operationsdo seu novo projeto. -
Remova as importações de
my-api-typescript-runtimee, em vez disso, importe o tipo de operação do TypeScript Server SDK gerado, bem como oServiceContext, por exemplo:import {deleteShoppingListHandler,DeleteShoppingListChainedHandlerFunction,INTERCEPTORS,Response,LoggingInterceptor,} from 'myapi-typescript-runtime';import { DeleteShoppingList as DeleteShoppingListOperation } from '../generated/ssdk/index.js';import { ServiceContext } from '../context.js'; -
Exclua a exportação do wrapper do handler
export const handler = deleteShoppingListHandler(...INTERCEPTORS,deleteShoppingList,); -
Atualize a assinatura do seu handler de operação para usar o SSDK:
export const deleteShoppingList: DeleteShoppingListChainedHandlerFunction = async (request) => {export const DeleteShoppingList: DeleteShoppingListOperation<ServiceContext> = async (input, ctx) => { -
Substitua o uso do
LoggingInterceptorporctx.logger. (Também se aplica aos interceptors de métricas e rastreamento):LoggingInterceptor.getLogger(request).info('...');ctx.logger.info('...'); -
Atualize as referências aos parâmetros de entrada. Como o SSDK fornece tipos que correspondem exatamente ao seu modelo Smithy (em vez de agrupar parâmetros de caminho/consulta/cabeçalho separadamente do parâmetro de corpo), atualize quaisquer referências de entrada de acordo:
const shoppingListId = request.input.requestParameters.shoppingListId;const shoppingListId = input.shoppingListId; -
Remova o uso de
Response. Em vez disso, apenas retornamos objetos simples no SSDK.return Response.success({ shoppingListId });return { shoppingListId };Também não lançamos ou retornamos mais
Response, em vez disso, lançamos os erros gerados do 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' }); -
Atualize quaisquer importações para usar a sintaxe ESM, ou seja, adicionando a extensão
.jsàs importações relativas. -
Adicione a operação 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,};
Migração de Handler da 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, };};Geramos o projeto Smithy API com o nome api inicialmente, pois queríamos que fosse adicionado a packages/api para consistência com o projeto PDK. Como nossa Smithy API agora define service MyApi em vez de service Api, precisamos atualizar quaisquer instâncias de getApiServiceHandler com getMyApiServiceHandler.
Faça essa alteração em 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);E em 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);Além disso, atualize packages/api/backend/project.json e atualize metadata.apiName para my-api:
"metadata": { "generator": "ts#smithy-api", "apiName": "api", "apiName": "my-api", "auth": "iam", "modelProject": "@shopping-list/api-model", "ports": [3001] },Verificar com uma Compilação
Seção intitulada “Verificar com uma Compilação”Agora podemos compilar o projeto para verificar se a migração funcionou até agora:
pnpm nx run-many --target buildyarn nx run-many --target buildnpx nx run-many --target buildbunx nx run-many --target buildMigrar o Website
Seção intitulada “Migrar o Website”O CloudscapeReactTsWebsiteProject usado na aplicação de lista de compras configurou um website React com CloudScape e autenticação Cognito integrados.
Este tipo de projeto aproveitava create-react-app, que agora está obsoleto. Para migrar o website neste guia, usaremos o gerador ts#website, que usa tecnologias mais modernas e suportadas, nomeadamente Vite.
Como parte da migração, também migraremos do React Router configurado pelo PDK para o TanStack Router, que adiciona segurança de tipo adicional ao roteamento do website.
Gerar um Website React
Seção intitulada “Gerar um Website React”Execute o gerador ts#website com framework definido como react para configurar seu projeto de website em packages/website. Como a aplicação de lista de compras é construída com componentes CloudScape, também definimos ux como cloudscape (o padrão é 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-interactiveVocê também pode realizar uma execução simulada para ver quais arquivos seriam alterados
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 o Nx Console VSCode Plugin se ainda não o fez
- Abra o console Nx no VSCode
- Clique em
Generate (UI)na seção "Common Nx Commands" - Procure por
@aws/nx-plugin - ts#website - Preencha os parâmetros obrigatórios
- name: website
- framework: react
- ux: cloudscape
- Clique em
Generate
Adicionar Autenticação Cognito
Seção intitulada “Adicionar Autenticação Cognito”O gerador de website React acima não inclui autenticação cognito por padrão como o CloudscapeReactTsWebsiteProject, em vez disso, é adicionado explicitamente através do gerador 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-interactiveVocê também pode realizar uma execução simulada para ver quais arquivos seriam alterados
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 o Nx Console VSCode Plugin se ainda não o fez
- Abra o console Nx no VSCode
- Clique em
Generate (UI)na seção "Common Nx Commands" - Procure por
@aws/nx-plugin - ts#website#auth - Preencha os parâmetros obrigatórios
- project: website
- cognitoDomain: shopping-list
- Clique em
Generate
Isso adiciona componentes React que gerenciam os redirecionamentos apropriados para garantir que os usuários façam login usando a UI hospedada do Cognito. Isso também adiciona um construto CDK para implantar os recursos do Cognito em packages/common/constructs, chamado UserIdentity.
Conectar o Website à API
Seção intitulada “Conectar o Website à API”No PDK, você podia passar os projetos Projen fornecidos uns aos outros para acionar a geração de código de integração. Isso foi usado na aplicação de lista de compras para configurar o website para poder integrar com a API.
Com o Nx Plugin for AWS, a integração de API é suportada através do gerador connection. Em seguida, usamos este gerador para que nosso website possa invocar nossa 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-interactiveVocê também pode realizar uma execução simulada para ver quais arquivos seriam alterados
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 o Nx Console VSCode Plugin se ainda não o fez
- Abra o console Nx no VSCode
- Clique em
Generate (UI)na seção "Common Nx Commands" - Procure por
@aws/nx-plugin - connection - Preencha os parâmetros obrigatórios
- sourceProject: website
- targetProject: api
- Clique em
Generate
Isso gera os provedores de cliente necessários e alvos de build para que seu website chame sua API através de um cliente TypeScript gerado.
Adicionar Dependência AWS Northstar
Seção intitulada “Adicionar Dependência AWS Northstar”O CloudscapeReactTsWebsiteProject incluía automaticamente uma dependência em @aws-northstar/ui que é usada em nossa aplicação de lista de compras, então a adicionamos ao projeto @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 inclui um componente de editor de código que depende de ace-builds, usando uma importação específica do webpack que o Vite não consegue resolver. Como nossa aplicação de lista de compras não usa este componente, o excluímos do bundle adicionando-o à configuração external dentro das opções build existentes em 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 os Componentes e Páginas
Seção intitulada “Mover os Componentes e Páginas”A aplicação de lista de compras tem um componente chamado CreateItem e duas páginas, ShoppingList e ShoppingLists. Vamos migrá-los para o novo website, fazendo alguns ajustes, já que estamos usando TanStack Router e o gerador de código de cliente TypeScript do Nx Plugin for AWS.
-
Copie
packages/website/src/components/CreateItem/index.tsxdo projeto PDK para o mesmo local exato no novo projeto. -
Copie
packages/website/src/pages/ShoppingLists/index.tsxparapackages/website/src/routes/index.tsx, já queShoppingListsé nossa página inicial e usamos roteamento baseado em arquivos com TanStack router. -
Copie
packages/website/src/pages/ShoppingList/index.tsxparapackages/website/src/routes/$shoppingListId.tsx, já queShoppingListera a página que queremos mostrar na rota/:shoppingListId.
Observe que agora você terá alguns erros de build visíveis no seu IDE, precisaremos fazer mais algumas alterações para se adequar ao novo framework, descritas abaixo.
Migrar do React Router para o TanStack Router
Seção intitulada “Migrar do React Router para o TanStack Router”Como estamos usando roteamento baseado em arquivos, podemos usar o servidor de desenvolvimento local do website para gerenciar automaticamente a geração da configuração de rotas.
Vamos iniciar o servidor local do website:
pnpm nx dev websiteyarn nx dev websitenpx nx dev websitebunx nx dev websiteVocê verá alguns erros, mas o servidor local do website deve iniciar na porta 4200, assim como o servidor local da API Smithy na porta 3001.
Siga as etapas abaixo em ambos routes/index.tsx e routes/$shoppingListId.tsx para migrar para o TanStack Router:
-
Adicione
createFileRoutepara registrar cada rota: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,});Depois de salvar o arquivo, você notará que os erros de tipo com a chamada para
createFileRoutedesapareceram. -
Substitua o hook
useNavigate.Atualize a importação:
import { useNavigate } from 'react-router-dom';import { useNavigate } from '@tanstack/react-router';Atualize as chamadas para o método
navigate(retornado poruseNavigate) para passar as rotas com segurança de tipo:navigate(`/${cell.shoppingListId}`);navigate({to: '/$shoppingListId',params: { shoppingListId: cell.shoppingListId },}); -
Substitua o hook
useParams.Remova a importação:
import { useParams } from 'react-router-dom';Atualize as chamadas para
useParamscom o hook fornecido pelaRoutecriada acima. Agora eles têm segurança de tipo!const { shoppingListId } = useParams();const { shoppingListId } = Route.useParams();
Corrigir Importações de Componentes
Seção intitulada “Corrigir Importações de Componentes”Como nossos arquivos de rota não estão tão profundamente aninhados na árvore de arquivos como estavam em nosso projeto PDK, precisamos corrigir a importação de CreateItem em ambos routes/index.tsx e routes/$shoppingListId.tsx:
import CreateItem from "../../components/CreateItem";import CreateItem from "../components/CreateItem";O AppLayoutContext também é fornecido em um local ligeiramente diferente em nosso novo projeto:
import { AppLayoutContext } from "../../layouts/App";import { AppLayoutContext } from "../components/AppLayout";Migrar para usar o novo Cliente TypeScript Gerado
Seção intitulada “Migrar para usar o novo Cliente TypeScript Gerado”Estamos chegando mais perto agora! Em seguida, precisamos migrar para usar o cliente TypeScript fornecido pelo Nx Plugin for AWS, que tem algumas melhorias em comparação com Type Safe API. Para conseguir isso, siga as etapas abaixo
-
Importe o novo cliente e tipos gerados em vez dos antigos, por exemplo:
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";Observe que
routes/$shoppingListId.tsximporta o tipoShoppingListcomo_ShoppingList- nesse arquivo devemos fazer o mesmo, mas novamente importando detypes.gen.Observe também que importamos os hooks relevantes diretamente de
@tanstack/react-query, já que o cliente gerado fornece métodos para gerar opções para hooks TanStack query, em vez de wrappers de hooks. -
Instancie os novos hooks TanStack Query, por exemplo:
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(),); -
Remova o wrapper
<operation>RequestContentpara chamadas a operações que aceitam parâmetros no corpo da requisição:await putShoppingList.mutateAsync({putShoppingListRequestContent: {name: item,},});
Migrar do TanStack Query v4 para v5
Seção intitulada “Migrar do TanStack Query v4 para v5”Há alguns erros restantes para corrigir devido a diferenças entre TanStack Query v4 (usado pelo PDK) e v5 que o gerador connection adicionou:
-
Substitua
isLoadingporisPendingpara mutações, por exemplo:putShoppingList.isLoadingputShoppingList.isPending -
A aplicação de lista de compras fez uso do
InfiniteQueryTablede@aws-northstar/uique espera um tipo do TanStack Query v4. Isso na verdade funciona com consultas infinitas do v5, então podemos apenas suprimir o erro de tipo:<InfiniteQueryTablequery={getShoppingLists}query={getShoppingLists as any}
Visitar o Website Local
Seção intitulada “Visitar o Website Local”Agora você pode visitar o website local em http://localhost:4200/
O website deve carregar agora que tudo foi migrado! Como a única infraestrutura de que a aplicação de lista de compras depende além de API, Website e Identity é a tabela DynamoDB - se você tiver uma tabela DynamoDB chamada shopping_list na região, e credenciais AWS locais que possam acessá-la, o website será totalmente funcional!
Se não, tudo bem, vamos migrar a infraestrutura a seguir.
Migração da 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 a Infraestrutura
Seção intitulada “Migrar a Infraestrutura”O último projeto que precisamos migrar para nossa aplicação de lista de compras é o InfrastructureTsProject. Este é um projeto TypeScript CDK, para o qual o equivalente do Nx Plugin for AWS é o gerador ts#infra.
Além dos projetos Projen, o PDK também fornecia constructs CDK dos quais esses projetos dependem. Vamos migrar a aplicação de lista de compras desses constructs CDK também, em favor dos gerados pelo Nx Plugin for AWS.
Gerar um Projeto de Infraestrutura TypeScript CDK
Seção intitulada “Gerar um Projeto de Infraestrutura TypeScript CDK”Execute o gerador ts#infra para configurar seu projeto de infraestrutura em 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-interactiveVocê também pode realizar uma execução simulada para ver quais arquivos seriam alterados
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 o Nx Console VSCode Plugin se ainda não o fez
- Abra o console Nx no VSCode
- Clique em
Generate (UI)na seção "Common Nx Commands" - Procure por
@aws/nx-plugin - ts#infra - Preencha os parâmetros obrigatórios
- name: infra
- Clique em
Generate
Migrar a Infraestrutura CDK
Seção intitulada “Migrar a Infraestrutura CDK”A aplicação de lista de compras PDK instanciou os seguintes constructs dentro do stack da aplicação CDK:
DatabaseConstructpara a tabela DynamoDB que armazena listas de comprasUserIdentitypara recursos Cognito, importado diretamente do PDKMyApipara implantar a API Smithy, que usou o construct TypeScript CDK gerado com integrações type-safe, dependendo do construct CDKTypeSafeRestApido PDK por baixo dos panos.Websitepara implantar o Website, envolvendo o construct CDKStaticWebsitedo PDK.
Em seguida, vamos migrar cada um deles para o novo projeto.
Copiar o Application Stack
Seção intitulada “Copiar o Application Stack”Copie packages/infra/src/stacks/application-stack.ts da aplicação de lista de compras PDK para o exato mesmo local em seu novo projeto. Você verá alguns erros TypeScript que abordaremos abaixo.
Copiar o Database Construct
Seção intitulada “Copiar o Database Construct”A aplicação de lista de compras PDK tinha um construct Database em packages/src/constructs/database.ts. Copie isso para o exato mesmo local em seu novo projeto.
Como o Nx Plugin for AWS usa Checkov para testes de segurança, que é um pouco mais rigoroso que o PDK Nag, também precisamos adicionar algumas supressões:
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',);Em application-stack.ts, atualize a importação para o DatabaseConstruct para usar a sintaxe ESM:
import { DatabaseConstruct } from '../constructs/database';import { DatabaseConstruct } from '../constructs/database.js';Migrar o UserIdentity Construct
Seção intitulada “Migrar o UserIdentity Construct”O construct UserIdentity geralmente pode ser trocado sem alterações ajustando as importações.
import { UserIdentity } from "@aws/pdk/identity";import { UserIdentity } from '@shopping-list/common-constructs';...const userIdentity = new UserIdentity(this, `${id}UserIdentity`);Note que os constructs subjacentes usados pelo novo construct UserIdentity são fornecidos diretamente de aws-cdk-lib, onde o PDK usava @aws-cdk/aws-cognito-identitypool-alpha.
Migrar o API Construct
Seção intitulada “Migrar o API Construct”A aplicação de lista de compras PDK tinha um construct em constructs/apis/myapi.ts que instanciava um construct CDK que o Type Safe API gerou a partir do seu modelo Smithy.
Além deste construct, como o projeto PDK usava o trait @handler, constructs CDK de função lambda gerados também foram gerados.
Como o Type Safe API, o Nx Plugin for AWS fornece type-safety para integrações baseadas no seu modelo Smithy, no entanto, é alcançado de uma maneira muito mais simples e flexível. Em vez de gerar um construct CDK inteiro em tempo de compilação, apenas “metadados” mínimos são gerados, que o packages/common/constructs/src/app/apis/api.ts usa de forma genérica. Você pode aprender mais sobre como usar o construct no guia do gerador ts#smithy-api.
Siga os passos abaixo:
-
Instancie o construct
Apiemapplication-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(),});Note aqui que usamos
Api.defaultIntegrations(this).build()- o comportamento padrão é criar uma função lambda para cada operação em nossa API, que é o mesmo comportamento que tínhamos emmyapi.ts. -
Conceda permissões para as funções lambda acessarem a tabela DynamoDB.
Na aplicação de lista de compras PDK, o
DatabaseConsructfoi passado paraMyApi, e ele gerenciava a adição das permissões relevantes a cada construct de função gerado. Faremos isso diretamente no arquivoapplication-stack.tsacessando a propriedade type-safeintegrationsdo 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)); -
Conceda permissões para usuários autenticados invocarem a API.
Dentro do
myapi.tsda aplicação PDK, usuários autenticados também receberam permissões IAM para invocar a API. Faremos o equivalente emapplication-stack.ts:stacks/application-stack.ts api.grantInvokeAccess(userIdentity.identityPool.authenticatedRole);
Migrar o Website Construct
Seção intitulada “Migrar o Website Construct”Finalmente, adicionamos o construct Website de packages/common/constructs/src/app/static-websites/website.ts a application-stack.ts, já que este é o equivalente do packages/infra/src/constructs/websites/website.ts da aplicação 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');Note que não passamos a identidade ou API para o website - a configuração de runtime é gerenciada dentro de cada construct fornecido pelo Nx Plugin for AWS, onde UserIdentity e Api registram os valores necessários, e Website gerencia a implantação em /runtime-config.json no seu site estático.
Vamos construir o projeto agora que migramos todas as partes relevantes da base de código para nosso novo projeto.
pnpm nx run-many --target buildyarn nx run-many --target buildnpx nx run-many --target buildbunx nx run-many --target buildAgora que temos nossa base de código totalmente migrada, podemos olhar para implantá-la. Existem dois caminhos que podemos seguir neste ponto.
Recursos Totalmente Novos (Simples)
Seção intitulada “Recursos Totalmente Novos (Simples)”A abordagem mais simples é tratar isso como uma aplicação completamente nova, o que significa que vamos “começar de novo” com uma nova tabela DynamoDB e um novo Cognito User Pool - perdendo todos os usuários e suas listas de compras. Para esta abordagem, simplesmente:
-
Exclua a tabela DynamoDB chamada
shopping_list -
Implante a nova aplicação:
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/*
🎉 E pronto! 🎉
Migrar Recursos com Estado Existentes sem Interrupção (Mais Complexo)
Seção intitulada “Migrar Recursos com Estado Existentes sem Interrupção (Mais Complexo)”Na realidade, é mais provável que você queira migrar recursos AWS existentes para que sejam gerenciados pela nova base de código, evitando qualquer tempo de inatividade para seus clientes.
Para nossa aplicação de lista de compras, os recursos com estado que nos importam são a tabela DynamoDB que contém as listas de compras de nossos usuários, e o User Pool que contém os detalhes de todos os nossos usuários registrados. Nosso plano de alto nível será reter esses dois recursos-chave e movê-los para que sejam gerenciados por nossa nova pilha, e então atualizar o DNS para apontar para nosso novo site (e API se exposta aos clientes).
-
Atualize sua nova aplicação para referenciar os recursos existentes que você deseja reter.
Para a aplicação de lista de compras, fazemos isso para a tabela DynamoDB
constructs/database.ts this.shoppingListTable = new Table(this, 'ShoppingList', {...this.shoppingListTable = Table.fromTableName(this,'ShoppingList','shopping_list',);E para o 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>',); -
Construa e implante a nova aplicação:
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/*Agora temos nossa nova aplicação configurada referenciando os recursos existentes, ainda não recebendo nenhum tráfego.
-
Execute testes de integração completos para garantir que a nova aplicação funcione conforme esperado. Para a aplicação de lista de compras, carregue o site e verifique se você pode fazer login e criar, visualizar, editar e excluir listas de compras.
-
Reverta as alterações que referenciam os recursos existentes em sua nova aplicação, mas não as implante ainda.
constructs/database.ts this.shoppingListTable = new Table(this, 'ShoppingList', {...this.shoppingListTable = Table.fromTableName(this,'ShoppingList','shopping_list',);E para o 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>',);E então execute uma construção
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 -
Use
cdk importna pastapackages/infrada sua nova aplicação para ver quais recursos seremos solicitados a importar.New Application cd packages/infrapnpm exec cdk import shopping-list-infra-sandbox/Application --forcePercorra os prompts pressionando enter. A importação falhará porque os recursos são gerenciados por outra pilha - isso é esperado, fizemos este passo apenas para confirmar quais recursos precisaremos reter. Você verá uma saída como esta:
Terminal window 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) yIsso nos diz que na verdade existem 3 recursos que precisaremos importar para nossa nova pilha.
-
Atualize seu projeto PDK antigo para definir
RemovalPolicycomoRETAINpara os recursos descobertos no passo anterior. No momento em que este texto foi escrito, este é o padrão tanto para o User Pool quanto para a tabela DynamoDB, mas precisamos atualizá-lo para o SMS Role que descobrimos acima: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); -
Implante seu projeto PDK para que as políticas de remoção sejam aplicadas
PDK Application cd packages/infranpx projen deploy -
Dê uma olhada no console do CloudFormation e registre os valores para os quais você foi solicitado no passo
cdk importacima- O User Pool ID, por exemplo
us-west-2_XXXXX - O SMS Role Name, por exemplo
infra-sandbox-UserIdentityUserPoolsmsRoleXXXXXX
- O User Pool ID, por exemplo
-
Atualize seu projeto PDK para referenciar os recursos existentes em vez de criá-los
constructs/database.ts this.shoppingListTable = new Table(this, 'ShoppingList', {...this.shoppingListTable = Table.fromTableName(this,'ShoppingList','shopping_list',);E para o 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,}); -
Implante seu projeto PDK novamente, isso significará que os recursos não são mais gerenciados pela pilha CloudFormation do nosso projeto PDK.
PDK Application cd packages/infranpx projen deploy -
Agora que os recursos não são gerenciados, podemos executar
cdk importem nossa nova aplicação para realmente realizar a importação:New Application cd packages/infrapnpm exec cdk import shopping-list-infra-sandbox/Application --forceDigite os valores quando solicitado, a importação deve ser concluída com sucesso.
-
Implante a nova aplicação novamente para garantir que quaisquer alterações nesses recursos existentes (agora gerenciados por sua nova pilha) sejam feitas:
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/* -
Execute um teste completo de sua nova aplicação novamente
-
Atualize os registros DNS para apontar para seu novo site (e API se necessário).
Recomendamos uma abordagem gradual usando o Weighted Routing do Route53, pelo qual uma fração das solicitações é direcionada para a nova aplicação no início. À medida que você monitora suas métricas, pode aumentar o peso para a nova aplicação até que nenhum tráfego seja enviado para sua aplicação PDK antiga.
Se você não tiver nenhum DNS e usou os domínios gerados automaticamente para o site e API, você sempre pode considerar fazer proxy de solicitações (por exemplo, via uma origem HTTP do CloudFront ou integração(ões) HTTP do API Gateway).
-
Monitore as métricas da aplicação PDK para garantir que não haja tráfego e, finalmente, destrua a pilha CloudFormation antiga:
Terminal window cd packages/infranpx projen destroy
Isso foi um pouco mais envolvido, mas migramos nossos usuários perfeitamente para a nova aplicação! 🎉🎉🎉
Agora temos os novos benefícios do Nx Plugin for AWS sobre o PDK:
- Construções mais rápidas
- Suporte ao desenvolvimento local de API
- Uma base de código amigável para vibe-coding (experimente nosso servidor MCP!)
- Código cliente/servidor type-safe mais intuitivo
- E muito mais!
Perguntas Frequentes
Seção intitulada “Perguntas Frequentes”Esta seção fornece orientações para recursos do PDK que não são cobertos pelo exemplo de migração acima.
Como regra geral ao migrar do PDK, recomendamos iniciar qualquer projeto com um Nx Workspace, dadas suas semelhanças com o PDK Monorepo. Também recomendamos usar nossos geradores como primitivos sobre os quais construir quaisquer novos tipos.
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
Seção intitulada “CDK Graph”CDK Graph constrói grafos dos seus recursos CDK conectados e fornecia dois plugins:
Diagram Plugin
Seção intitulada “Diagram Plugin”O CDK Graph Diagram Plugin gera diagramas de arquitetura AWS a partir da sua infraestrutura CDK.
Para uma abordagem determinística similar, uma alternativa viável é CDK-Dia.
Com os avanços em IA Generativa, muitos modelos de fundação são capazes de criar diagramas de alta qualidade a partir da sua infraestrutura CDK. Recomendamos experimentar o AWS Diagram MCP Server. Confira este post do blog para um passo a passo.
Threat Composer Plugin
Seção intitulada “Threat Composer Plugin”O CDK Graph Threat Composer Plugin gera um Threat Composer inicial de modelo de ameaças a partir do seu código CDK.
Este plugin funcionava simplesmente filtrando um modelo de ameaças base contendo exemplos de ameaças, e filtrando-os com base nos recursos que sua stack utilizava.
Se você está interessado nesses exemplos específicos de ameaças, você pode copiar e filtrar o modelo de ameaças base, ou usá-lo como contexto para ajudar um modelo de fundação a gerar um similar.
AWS Arch
Seção intitulada “AWS Arch”AWS Arch fornecia mapeamentos entre recursos do CloudFormation e seus ícones de arquitetura associados para o CDK Graph acima.
Consulte a página de Ícones de Arquitetura AWS para recursos relacionados a ícones. Diagrams também fornece uma maneira de construir diagramas como código.
Se você estava usando isso diretamente, considere fazer um fork do projeto e assumir a propriedade!
Pipeline
Seção intitulada “Pipeline”O PDK fornecia um PDKPipelineProject que configurava um projeto de infraestrutura CDK e fazia uso de um construto CDK que encapsulava alguns recursos de CDK Pipelines.
Para migrar disso, você pode usar os construtos CDK Pipelines diretamente. Na prática, no entanto, é provavelmente mais direto usar algo como GitHub actions ou GitLab CI/CD, onde você define CDK Stages e executa o comando deploy para o estágio apropriado diretamente.
PDK Nag
Seção intitulada “PDK Nag”PDK Nag envolve o CDK Nag e fornece um conjunto de regras específicas para construção de protótipos.
Para migrar do PDK Nag, use o CDK Nag diretamente. Se você precisar do mesmo conjunto de regras, pode criar um “pack” próprio seguindo a documentação aqui.
Type Safe API
Seção intitulada “Type Safe API”Os componentes mais comumente usados do Type Safe API são cobertos no exemplo de migração acima, no entanto, existem outros recursos, para os quais os detalhes de migração estão abaixo.
APIs Modeladas com OpenAPI
Seção intitulada “APIs Modeladas com OpenAPI”O Nx Plugin for AWS suporta APIs modeladas em Smithy, mas não aquelas modeladas diretamente em OpenAPI. O gerador ts#smithy-api é um bom ponto de partida que você pode então modificar. Você pode definir sua especificação OpenAPI na pasta src do projeto model em vez de Smithy, e modificar o build.Dockerfile para usar sua ferramenta de geração de código desejada para clientes/servidores se eles não estiverem disponíveis no NPM. Se suas ferramentas desejadas estiverem no NPM, você pode simplesmente instalá-las como dependências de desenvolvimento no seu workspace Nx e chamá-las diretamente como targets de build do Nx.
Backend
Seção intitulada “Backend”Para backends type-safe modelados em OpenAPI, você pode considerar usar um dos OpenAPI Generator Server Generators. Estes não geram diretamente para AWS Lambda, mas você pode usar o AWS Lambda Web Adapter para preencher a lacuna para muitos deles.
Para clientes TypeScript, você pode usar o gerador ts#website e o gerador connection com um exemplo de ts#api (com framework definido como smithy) para ver como os clientes são gerados e integrados com um website. Isso configura targets de build que geram clientes invocando nossos geradores open-api#ts-client ou open-api#ts-hooks. Você pode usar esses geradores você mesmo apontando-os para sua Especificação OpenAPI.
Para outras linguagens, você também pode verificar se algum dos geradores do OpenAPI Generator atende às suas necessidades.
Você também pode construir um gerador personalizado usando o gerador ts#nx-generator. Consulte a documentação desse gerador para detalhes sobre como gerar código a partir de OpenAPI. Você pode usar os templates do Nx Plugin for AWS como ponto de partida. Você também pode até consultar os templates da base de código do PDK para mais inspiração, observando que a estrutura de dados na qual os templates operam é um pouco diferente do Nx Plugin for AWS.
APIs Modeladas com TypeSpec
Seção intitulada “APIs Modeladas com TypeSpec”Para TypeSpec, a seção acima para OpenAPI também se aplica. Você pode começar gerando um ts#smithy-api, instalar o compilador TypeSpec e pacotes OpenAPI no seu workspace Nx, e atualizar o target compile do projeto model para executar tsp compile em vez disso, garantindo que ele produza uma especificação OpenAPI no diretório dist.
Backend
Seção intitulada “Backend”A abordagem recomendada seria usar o TypeSpec HTTP Server generator for JavaScript para gerar seu código de servidor, já que isso funciona diretamente no seu modelo TypeSpec.
Você pode usar o AWS Lambda Web Adapter para executar o servidor gerado no AWS Lambda.
Você também pode usar qualquer uma das opções OpenAPI acima.
TypeSpec tem seus próprios geradores de código para clientes nas três linguagens suportadas pelo Type Safe API:
A seção OpenAPI acima também se aplica, já que TypeSpec pode compilar para OpenAPI.
APIs Modeladas com Smithy
Seção intitulada “APIs Modeladas com Smithy”O exemplo de migração acima descreve a migração para usar o gerador ts#smithy-api. Esta seção cobre as opções para backends e clientes Python e Java.
Backend
Seção intitulada “Backend”O Smithy code generator for Java. Este tem um gerador de servidor Java, bem como um adaptador para executar o servidor Java gerado no AWS Lambda.
Smithy não tem um gerador de servidor para Python, então você precisará passar pelo OpenAPI. Consulte a seção acima sobre APIs Modeladas com OpenAPI para opções potenciais.
O Smithy code generator for Java. Este tem um gerador de cliente Java.
Para clientes Python, você pode conferir Smithy Python.
Para TypeScript, confira Smithy TypeScript, ou use a mesma abordagem que adotamos no ts#smithy-api passando pelo OpenAPI (optamos por isso, pois nos dá consistência entre APIs tRPC, FastAPI e Smithy via hooks TanStack Query).
Smithy Shape Library
Seção intitulada “Smithy Shape Library”Type Safe API forneceu um tipo de projeto Projen chamado SmithyShapeLibraryProject que configurava um projeto que continha modelos Smithy que podiam ser reutilizados por múltiplas APIs baseadas em Smithy.
O equivalente é o gerador smithy#project com type definido 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=shapesVocê também pode realizar uma execução simulada para ver quais arquivos seriam alterados
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 o Nx Console VSCode Plugin se ainda não o fez
- Abra o console Nx no VSCode
- Clique em
Generate (UI)na seção "Common Nx Commands" - Procure por
@aws/nx-plugin - smithy#project - Preencha os parâmetros obrigatórios
- name: my-shapes
- type: shapes
- Clique em
Generate
Mova as shapes do seu SmithyShapeLibraryProject para a pasta src do projeto gerado, então consulte o guia do projeto Smithy para como conectar a biblioteca como uma dependência do modelo da sua API.
Interceptors
Seção intitulada “Interceptors”Type Safe API forneceu os seguintes interceptors padrão:
- Interceptors de logging, tracing e métricas usando Powertools for AWS Lambda
- Interceptor try-catch para lidar com exceções não capturadas
- Interceptor CORS para retornar cabeçalhos CORS
O gerador ts#smithy-api instrumenta logging, tracing e métricas com Powertools for AWS Lambda usando Middy. O comportamento do interceptor try-catch está integrado ao Smithy TypeScript SSDK, e os cabeçalhos CORS são adicionados em handler.ts.
Para interceptors de logging, tracing e métricas em qualquer linguagem, use Powertools for AWS Lambda diretamente.
Para migrar interceptors personalizados, recomendamos usar as seguintes bibliotecas:
- TypeScript - Middy
- Python - Powertools for AWS Lambda Middleware Factory
- Java - Instrumente métodos antes/depois da sua lógica de negócios usando aws-lambda-java-libs para uma abordagem simples, ou considere AspectJ para construir seu middleware como anotações.
Geração de Documentação
Seção intitulada “Geração de Documentação”Type Safe API forneceu geração de documentação usando Redocly CLI. Isso é muito fácil de adicionar a um projeto existente depois de migrá-lo conforme acima.
-
Instale o 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 -
Adicione um target de geração de documentação ao seu projeto
modelusandoredocly build-docs, por exemplo: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"]}}
Você também pode considerar os OpenAPI Generator documentation generators.
Mock Integrations
Seção intitulada “Mock Integrations”Type Safe API gerou mocks para você dentro de seu pacote de infraestrutura gerado.
Você pode migrar para JSON Schema Faker que pode criar os dados mock baseados em JSON Schemas. Isso pode funcionar diretamente em uma especificação OpenAPI, e tem uma CLI que você pode executar como parte do build do seu projeto model.
Você pode atualizar sua infraestrutura CDK para ler o arquivo JSON gerado pelo JSON Schema Faker, e retornar a MockIntegration apropriada do API Gateway para uma integração, baseado no metadata.gen.ts gerado (assumindo que você usou o gerador ts#smithy-api).
Backends de Linguagem Mista
Seção intitulada “Backends de Linguagem Mista”Type Safe API suportava implementar APIs com uma mistura de diferentes linguagens no backend. Isso também pode ser alcançado fornecendo “overrides” para integrações ao instanciar seu construto de API no 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(),});Você precisará criar um “stub” do seu service/router para que seu serviço compile se estiver usando o ts#smithy-api e o TypeScript Server SDK, por exemplo:
export const Service: ApiService<ServiceContext> = { ... Echo: () => { throw new Error(`Not Implemented`); },};Validação de Entrada
Seção intitulada “Validação de Entrada”Type Safe API adicionou validação nativa do API Gateway para corpos de requisição baseada na sua especificação OpenAPI, já que usava o construto SpecRestApi internamente.
Com o gerador ts#smithy-api, a validação é realizada pelo próprio Server SDK. Isso é o mesmo para a maioria dos geradores de servidor.
Se você gostaria de implementar validação nativa do API Gateway, você poderia fazê-lo modificando packages/common/constructs/src/core/api/rest-api.ts para ler o JSON schema relevante para o corpo de requisição de cada operação da sua especificação OpenAPI.
APIs WebSocket
Seção intitulada “APIs WebSocket”Infelizmente, não há um caminho de migração direto para a API websocket do Type Safe API usando API Gateway e Lambda com desenvolvimento de API orientado a modelo. No entanto, esta seção do guia visa pelo menos oferecer algumas ideias.
Considere usar AsyncAPI para modelar sua API em vez de OpenAPI ou TypeSpec, já que isso é projetado para lidar com APIs assíncronas. O AsyncAPI NodeJS Template pode gerar um backend websocket Node que você poderia hospedar no ECS por exemplo.
Você também pode considerar AppSync Events para infraestrutura, e usar Powertools. Este post do blog vale a pena ler!
Outra opção é usar APIs GraphQL com websockets no AppSync, para o qual temos uma issue no GitHub que você pode dar +1! Consulte o guia do desenvolvedor AppSync para detalhes e links para projetos de exemplo.
Você também pode considerar criar seus próprios geradores de código que interpretam as mesmas extensões de fornecedor que o Type Safe API. Consulte a seção APIs Modeladas com OpenAPI para detalhes sobre como construir geradores personalizados baseados em OpenAPI. Você pode encontrar os templates que o Type Safe API usa para handlers Lambda de API Gateway Websocket API aqui, e o cliente aqui.
Você também pode considerar migrar para usar o gerador ts#trpc-api para usar tRPC. No momento da escrita, ainda não temos suporte para subscriptions/streaming, mas se isso é algo que você precisa, adicione um +1 à nossa issue no GitHub rastreando isso.
Smithy é agnóstico de protocolo, mas ainda não tem suporte para o protocolo Websocket, consulte esta issue no GitHub rastreando o suporte.
Infraestrutura em Python ou Java
Seção intitulada “Infraestrutura em Python ou Java”O PDK suportava infraestrutura CDK escrita em Python e Java. Não oferecemos suporte para isso no Nx Plugin for AWS no momento da escrita.
O caminho recomendado seria migrar sua infraestrutura CDK para TypeScript, ou usar nossos geradores e migrar o pacote de construtos comuns para a linguagem desejada. Você pode usar IA Generativa para acelerar esse tipo de migração, por exemplo Kiro CLI. Você pode fazer com que um agente de IA itere na migração até que os templates CloudFormation sintetizados sejam idênticos.
O mesmo se aplica à infraestrutura gerada pelo Type Safe API em Python ou Java - você pode traduzir o construto genérico rest-api.ts do pacote de construtos comuns e implementar seu próprio gerador de metadados simples para sua linguagem de destino (consulte a seção APIs Modelled with OpenAPI).
Você pode usar o gerador py#project para um projeto Python base ao qual adicionar seu código CDK (e mover seu arquivo cdk.json, adicionando os targets relevantes). Você pode usar o plugin @nx/gradle do Nx para projetos Java, ou @jnxplus/nx-maven para Maven.
Uso do Projen
Seção intitulada “Uso do Projen”PDK foi construído sobre Projen. Projen e Nx Generators têm diferenças bastante fundamentais, o que significa que, embora seja tecnicamente possível combiná-los, isso provavelmente é um antipadrão. Projen gerencia arquivos de projeto como código de forma que eles não possam ser modificados diretamente, enquanto Nx generators fornecem arquivos de projeto uma vez e então o código pode ser livremente modificado.
Se você deseja continuar a usar Projen, pode implementar os tipos de projeto Projen desejados você mesmo. Para seguir padrões do Nx Plugin for AWS, você pode executar nossos generators ou examinar seu código-fonte no GitHub para ver como os tipos de projeto desejados são construídos e implementar as partes relevantes usando as primitivas do Projen.