Di chuyển từ AWS PDK
Hướng dẫn này sẽ đưa bạn qua một ví dụ về việc di chuyển dự án AWS PDK sang Nx Plugin for AWS, đồng thời cung cấp hướng dẫn chung về chủ đề này.
Di chuyển sang Nx Plugin for AWS mang lại các lợi ích sau so với PDK:
- Xây dựng nhanh hơn
- Dễ sử dụng hơn (UI và CLI)
- Thân thiện với vibe-coding (thử MCP server của chúng tôi!)
- Công nghệ hiện đại hơn
- Phát triển API và website cục bộ
- Kiểm soát tốt hơn (sửa đổi các file được cung cấp để phù hợp với trường hợp sử dụng của bạn)
- Và nhiều hơn nữa!
Ví dụ Di chuyển: Ứng dụng Danh sách Mua sắm
Phần tiêu đề “Ví dụ Di chuyển: Ứng dụng Danh sách Mua sắm”Trong hướng dẫn này, chúng ta sẽ sử dụng Ứng dụng Danh sách Mua sắm từ Hướng dẫn PDK làm dự án mục tiêu để di chuyển. Làm theo các bước trong hướng dẫn đó để tạo dự án mục tiêu nếu bạn muốn tự mình làm theo.
Ứng dụng danh sách mua sắm bao gồm các loại dự án PDK sau:
MonorepoTsProjectTypeSafeApiProjectCloudscapeReactTsWebsiteProjectInfrastructureTsProject
Tạo Workspace
Phần tiêu đề “Tạo Workspace”Để bắt đầu, chúng ta sẽ tạo một workspace mới cho dự án mới của mình. Mặc dù cực đoan hơn so với di chuyển tại chỗ, cách tiếp cận này mang lại cho chúng ta kết quả cuối cùng sạch sẽ nhất. Tạo một Nx workspace tương đương với việc sử dụng MonorepoTsProject của PDK:
npx create-nx-workspace@21.4.1 shopping-list --pm=pnpm --preset=@aws/nx-plugin@0.50.0 --iacProvider=CDK --ci=skip --aiAgentsnpx create-nx-workspace@21.4.1 shopping-list --pm=yarn --preset=@aws/nx-plugin@0.50.0 --iacProvider=CDK --ci=skip --aiAgentsnpx create-nx-workspace@21.4.1 shopping-list --pm=npm --preset=@aws/nx-plugin@0.50.0 --iacProvider=CDK --ci=skip --aiAgentsnpx create-nx-workspace@21.4.1 shopping-list --pm=bun --preset=@aws/nx-plugin@0.50.0 --iacProvider=CDK --ci=skip --aiAgentsMở thư mục shopping-list mà lệnh này tạo ra trong IDE yêu thích của bạn.
Di chuyển API
Phần tiêu đề “Di chuyển API”TypeSafeApiProject được sử dụng trong ứng dụng danh sách mua sắm đã tận dụng:
- Smithy làm ngôn ngữ mô hình hóa
- TypeScript để triển khai các operation
- Tạo hook TypeScript để tích hợp với website react
Do đó, chúng ta có thể sử dụng generator ts#smithy-api để cung cấp chức năng tương đương.
Tạo một TypeScript Smithy API
Phần tiêu đề “Tạo một TypeScript Smithy API”Chạy generator ts#smithy-api để thiết lập dự án api của bạn trong packages/api:
- Install the Nx Console VSCode Plugin if you haven't already
- Open the Nx Console in VSCode
- Click
Generate (UI)in the "Common Nx Commands" section - Search for
@aws/nx-plugin - ts#smithy-api - Fill in the required parameters
- name: api
- namespace: com.aws
- auth: IAM
- Click
Generate
pnpm nx g @aws/nx-plugin:ts#smithy-api --name=api --namespace=com.aws --auth=IAM --no-interactiveyarn nx g @aws/nx-plugin:ts#smithy-api --name=api --namespace=com.aws --auth=IAM --no-interactivenpx nx g @aws/nx-plugin:ts#smithy-api --name=api --namespace=com.aws --auth=IAM --no-interactivebunx nx g @aws/nx-plugin:ts#smithy-api --name=api --namespace=com.aws --auth=IAM --no-interactiveYou can also perform a dry-run to see what files would be changed
pnpm nx g @aws/nx-plugin:ts#smithy-api --name=api --namespace=com.aws --auth=IAM --no-interactive --dry-runyarn nx g @aws/nx-plugin:ts#smithy-api --name=api --namespace=com.aws --auth=IAM --no-interactive --dry-runnpx nx g @aws/nx-plugin:ts#smithy-api --name=api --namespace=com.aws --auth=IAM --no-interactive --dry-runbunx nx g @aws/nx-plugin:ts#smithy-api --name=api --namespace=com.aws --auth=IAM --no-interactive --dry-runBạn sẽ nhận thấy điều này tạo ra một dự án model, cũng như một dự án backend. Dự án model chứa mô hình Smithy của bạn, và backend chứa triển khai máy chủ của bạn.
Backend sử dụng Smithy Server Generator for TypeScript. Chúng ta sẽ khám phá điều này chi tiết hơn bên dưới.
Di chuyển Mô hình Smithy
Phần tiêu đề “Di chuyển Mô hình Smithy”Bây giờ chúng ta đã có cấu trúc cơ bản cho dự án Smithy API, chúng ta có thể di chuyển mô hình:
-
Xóa các tệp Smithy ví dụ đã được tạo trong
packages/api/model/src -
Sao chép mô hình của bạn từ thư mục
packages/api/model/src/main/smithycủa dự án PDK vào thư mụcpackages/api/model/srccủa dự án mới. -
Cập nhật tên dịch vụ và namespace trong
smithy-build.jsonđể khớp với ứng dụng PDK:smithy-build.json "plugins": {"openapi": {"service": "com.aws#MyApi",... -
Cập nhật dịch vụ trong
main.smithyđể thêm lỗiValidationException, điều này là bắt buộc khi sử dụng Smithy TypeScript Server SDK.main.smithy use smithy.framework#ValidationException/// My Shopping List API@restJson1service MyApi {version: "1.0"operations: [GetShoppingListsPutShoppingListDeleteShoppingList]errors: [BadRequestErrorNotAuthorizedErrorInternalFailureErrorValidationException]} -
Thêm tệp
extensions.smithyvàopackages/api/model/srcnơi chúng ta sẽ định nghĩa một trait cung cấp thông tin phân trang cho client được tạo:extensions.smithy $version: "2"namespace com.awsuse smithy.openapi#specificationExtension@trait@specificationExtension(as: "x-cursor")structure cursor {inputToken: Stringenabled: Boolean} -
Thêm trait
@cursormới vào operationGetShoppingListstrongget-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}Bất kỳ operation
@paginatednào cũng nên sử dụng@cursornếu bạn đang sử dụng client generator được cung cấp bởi Nx Plugin for AWS (thông qua generatorapi-connection). -
Cuối cùng, xóa trait
@handlerkhỏi tất cả các operation vì điều này không được hỗ trợ bởi Nx Plugin for AWS. Khi sử dụngts#smithy-api, chúng ta không cần các construct CDK hàm lambda được tự động tạo và các target đóng gói được tạo bởi trait này, vì chúng ta sử dụng một bundle duy nhất cho tất cả các hàm lambda.
Tại thời điểm này, hãy chạy một bản build để kiểm tra các thay đổi mô hình của chúng ta và đảm bảo rằng chúng ta có một số mã máy chủ được tạo để làm việc. Sẽ có một số lỗi trong dự án backend (@shopping-list/api) nhưng chúng ta sẽ giải quyết chúng tiếp theo.
pnpm nx run-many --target buildyarn nx run-many --target buildnpx nx run-many --target buildbunx nx run-many --target buildDi chuyển Lambda Handler
Phần tiêu đề “Di chuyển Lambda Handler”Bạn có thể coi dự án api/backend phần nào tương đương với dự án api/handlers/typescript của Type Safe API.
Một trong những khác biệt chính giữa Type Safe API và generator ts#smithy-api là các handler được triển khai bằng cách sử dụng Smithy Server Generator for TypeScript, thay vì các wrapper handler được tạo riêng của Type Safe API (được tìm thấy trong dự án api/generated/typescript/runtime).
Các lambda handler của ứng dụng danh sách mua sắm phụ thuộc vào package @aws-sdk/client-dynamodb, vì vậy hãy cài đặt nó trước:
pnpm add -w @aws-sdk/client-dynamodbyarn add @aws-sdk/client-dynamodbnpm install --legacy-peer-deps @aws-sdk/client-dynamodbbun install @aws-sdk/client-dynamodbSau đó, hãy sao chép tệp handlers/src/dynamo-client.ts từ dự án PDK vào backend/src/operations để nó có sẵn cho các handler của chúng ta.
Để di chuyển các handler, bạn có thể làm theo các bước chung sau:
-
Sao chép handler từ thư mục
packages/api/handlers/typescript/srccủa dự án PDK sang thư mụcpackages/api/backend/src/operationscủa dự án mới. -
Xóa các import
my-api-typescript-runtimevà thay vào đó import kiểu operation từ TypeScript Server SDK được tạo, cũng nhưServiceContextví dụ:import {deleteShoppingListHandler,DeleteShoppingListChainedHandlerFunction,INTERCEPTORS,Response,LoggingInterceptor,} from 'myapi-typescript-runtime';import { DeleteShoppingList as DeleteShoppingListOperation } from '../generated/ssdk/index.js';import { ServiceContext } from '../context.js'; -
Xóa export wrapper handler
export const handler = deleteShoppingListHandler(...INTERCEPTORS,deleteShoppingList,); -
Cập nhật chữ ký cho operation handler của bạn để sử dụng SSDK:
export const deleteShoppingList: DeleteShoppingListChainedHandlerFunction = async (request) => {export const DeleteShoppingList: DeleteShoppingListOperation<ServiceContext> = async (input, ctx) => { -
Thay thế việc sử dụng
LoggingInterceptorbằngctx.logger. (Cũng áp dụng cho các interceptor metrics và tracing):LoggingInterceptor.getLogger(request).info('...');ctx.logger.info('...'); -
Cập nhật các tham chiếu đến tham số đầu vào. Vì SSDK cung cấp các kiểu khớp chính xác với mô hình Smithy của bạn (thay vì nhóm các tham số path/query/header riêng biệt với tham số body), hãy cập nhật mọi tham chiếu đầu vào tương ứng:
const shoppingListId = request.input.requestParameters.shoppingListId;const shoppingListId = input.shoppingListId; -
Xóa việc sử dụng
Response. Thay vào đó, chúng ta chỉ trả về các đối tượng thuần túy trong SSDK.return Response.success({ shoppingListId });return { shoppingListId };Chúng ta cũng không còn throw hoặc return
Response, thay vào đó chúng ta throw các lỗi được tạo của 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' }); -
Cập nhật mọi import để sử dụng cú pháp ESM, cụ thể là thêm phần mở rộng
.jsvào các import tương đối. -
Thêm operation vào
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,};
Di chuyển Handler Danh sách Mua sắm
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, };};Chúng ta đã tạo dự án Smithy API với tên api ban đầu vì chúng ta muốn nó được thêm vào packages/api để đảm bảo tính nhất quán với dự án PDK. Vì Smithy API của chúng ta bây giờ định nghĩa service MyApi thay vì service Api, chúng ta cần cập nhật bất kỳ instance nào của getApiServiceHandler thành getMyApiServiceHandler.
Thực hiện thay đổi này trong 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);Và trong 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);Ngoài ra, cập nhật packages/api/backend/project.json và cập nhật metadata.apiName thành my-api:
"metadata": { "generator": "ts#smithy-api", "apiName": "api", "apiName": "my-api", "auth": "IAM", "modelProject": "@shopping-list/api-model", "ports": [3001] },Xác minh với Build
Phần tiêu đề “Xác minh với Build”Bây giờ chúng ta có thể build dự án để kiểm tra xem việc di chuyển đã hoạt động đến đâu:
pnpm nx run-many --target buildyarn nx run-many --target buildnpx nx run-many --target buildbunx nx run-many --target buildDi chuyển Website
Phần tiêu đề “Di chuyển Website”CloudscapeReactTsWebsiteProject được sử dụng trong ứng dụng danh sách mua sắm đã cấu hình một trang web React với CloudScape và xác thực Cognito được tích hợp sẵn.
Loại dự án này tận dụng create-react-app, hiện đã không còn được hỗ trợ. Để di chuyển trang web trong hướng dẫn này, chúng ta sẽ sử dụng trình tạo ts#react-website, sử dụng các công nghệ hiện đại và được hỗ trợ hơn, cụ thể là Vite.
Trong quá trình di chuyển, chúng ta cũng sẽ chuyển từ React Router được cấu hình sẵn của PDK sang TanStack Router, điều này bổ sung thêm tính an toàn về kiểu cho việc định tuyến trang web.
Tạo một Trang Web React
Phần tiêu đề “Tạo một Trang Web React”Chạy trình tạo ts#react-website để thiết lập dự án trang web của bạn trong packages/website:
- Install the Nx Console VSCode Plugin if you haven't already
- Open the Nx Console in VSCode
- Click
Generate (UI)in the "Common Nx Commands" section - Search for
@aws/nx-plugin - ts#react-website - Fill in the required parameters
- name: website
- Click
Generate
pnpm nx g @aws/nx-plugin:ts#react-website --name=website --no-interactiveyarn nx g @aws/nx-plugin:ts#react-website --name=website --no-interactivenpx nx g @aws/nx-plugin:ts#react-website --name=website --no-interactivebunx nx g @aws/nx-plugin:ts#react-website --name=website --no-interactiveYou can also perform a dry-run to see what files would be changed
pnpm nx g @aws/nx-plugin:ts#react-website --name=website --no-interactive --dry-runyarn nx g @aws/nx-plugin:ts#react-website --name=website --no-interactive --dry-runnpx nx g @aws/nx-plugin:ts#react-website --name=website --no-interactive --dry-runbunx nx g @aws/nx-plugin:ts#react-website --name=website --no-interactive --dry-runThêm Xác Thực Cognito
Phần tiêu đề “Thêm Xác Thực Cognito”Trình tạo trang web React ở trên không đi kèm xác thực cognito theo mặc định như CloudscapeReactTsWebsiteProject, thay vào đó nó được thêm vào một cách rõ ràng thông qua trình tạo ts#react-website#auth.
- Install the Nx Console VSCode Plugin if you haven't already
- Open the Nx Console in VSCode
- Click
Generate (UI)in the "Common Nx Commands" section - Search for
@aws/nx-plugin - ts#react-website#auth - Fill in the required parameters
- project: website
- cognitoDomain: shopping-list
- Click
Generate
pnpm nx g @aws/nx-plugin:ts#react-website#auth --project=website --cognitoDomain=shopping-list --no-interactiveyarn nx g @aws/nx-plugin:ts#react-website#auth --project=website --cognitoDomain=shopping-list --no-interactivenpx nx g @aws/nx-plugin:ts#react-website#auth --project=website --cognitoDomain=shopping-list --no-interactivebunx nx g @aws/nx-plugin:ts#react-website#auth --project=website --cognitoDomain=shopping-list --no-interactiveYou can also perform a dry-run to see what files would be changed
pnpm nx g @aws/nx-plugin:ts#react-website#auth --project=website --cognitoDomain=shopping-list --no-interactive --dry-runyarn nx g @aws/nx-plugin:ts#react-website#auth --project=website --cognitoDomain=shopping-list --no-interactive --dry-runnpx nx g @aws/nx-plugin:ts#react-website#auth --project=website --cognitoDomain=shopping-list --no-interactive --dry-runbunx nx g @aws/nx-plugin:ts#react-website#auth --project=website --cognitoDomain=shopping-list --no-interactive --dry-runĐiều này thêm các component React quản lý các chuyển hướng phù hợp để đảm bảo người dùng đăng nhập bằng giao diện người dùng được lưu trữ của Cognito. Điều này cũng thêm một construct CDK để triển khai các tài nguyên Cognito trong packages/common/constructs, được gọi là UserIdentity.
Kết Nối Trang Web với API
Phần tiêu đề “Kết Nối Trang Web với API”Trong PDK, bạn có thể truyền các dự án Projen được cung cấp cho nhau để kích hoạt mã tích hợp được tạo ra. Điều này đã được sử dụng trong ứng dụng danh sách mua sắm để cấu hình trang web có thể tích hợp với API.
Với Nx Plugin for AWS, tích hợp API được hỗ trợ thông qua trình tạo api-connection. Tiếp theo, chúng ta sử dụng trình tạo này để trang web của chúng ta có thể gọi Smithy API:
- Install the Nx Console VSCode Plugin if you haven't already
- Open the Nx Console in VSCode
- Click
Generate (UI)in the "Common Nx Commands" section - Search for
@aws/nx-plugin - api-connection - Fill in the required parameters
- sourceProject: website
- targetProject: api
- Click
Generate
pnpm nx g @aws/nx-plugin:api-connection --sourceProject=website --targetProject=api --no-interactiveyarn nx g @aws/nx-plugin:api-connection --sourceProject=website --targetProject=api --no-interactivenpx nx g @aws/nx-plugin:api-connection --sourceProject=website --targetProject=api --no-interactivebunx nx g @aws/nx-plugin:api-connection --sourceProject=website --targetProject=api --no-interactiveYou can also perform a dry-run to see what files would be changed
pnpm nx g @aws/nx-plugin:api-connection --sourceProject=website --targetProject=api --no-interactive --dry-runyarn nx g @aws/nx-plugin:api-connection --sourceProject=website --targetProject=api --no-interactive --dry-runnpx nx g @aws/nx-plugin:api-connection --sourceProject=website --targetProject=api --no-interactive --dry-runbunx nx g @aws/nx-plugin:api-connection --sourceProject=website --targetProject=api --no-interactive --dry-runĐiều này tạo ra các provider client cần thiết và các target build để trang web của bạn có thể gọi API của bạn thông qua một client TypeScript được tạo.
Thêm Phụ Thuộc AWS Northstar
Phần tiêu đề “Thêm Phụ Thuộc AWS Northstar”CloudscapeReactTsWebsiteProject tự động bao gồm phụ thuộc vào @aws-northstar/ui được sử dụng trong ứng dụng danh sách mua sắm của chúng ta, vì vậy chúng ta thêm nó ở đây:
pnpm add -w @aws-northstar/uiyarn add @aws-northstar/uinpm install --legacy-peer-deps @aws-northstar/uibun install @aws-northstar/uiDi Chuyển các Component và Page
Phần tiêu đề “Di Chuyển các Component và Page”Ứng dụng danh sách mua sắm có một component có tên CreateItem, và hai page, ShoppingList và ShoppingLists. Chúng ta sẽ di chuyển chúng sang trang web mới, thực hiện một số điều chỉnh vì chúng ta đang sử dụng TanStack Router và trình tạo mã client TypeScript của Nx Plugin for AWS.
-
Sao chép
packages/website/src/components/CreateItem/index.tsxtừ dự án PDK vào chính xác cùng vị trí trong dự án mới. -
Sao chép
packages/website/src/pages/ShoppingLists/index.tsxsangpackages/website/src/routes/index.tsx, vìShoppingListslà trang chủ của chúng ta và chúng ta sử dụng định tuyến dựa trên file với TanStack router. -
Sao chép
packages/website/src/pages/ShoppingList/index.tsxsangpackages/website/src/routes/$shoppingListId.tsx, vìShoppingListlà trang chúng ta muốn hiển thị trên route/:shoppingListId.
Lưu ý rằng bây giờ bạn sẽ có một số lỗi build hiển thị trong IDE của mình, chúng ta sẽ cần thực hiện thêm một vài thay đổi để phù hợp với framework mới, được nêu dưới đây.
Di Chuyển từ React Router sang TanStack Router
Phần tiêu đề “Di Chuyển từ React Router sang TanStack Router”Vì chúng ta đang sử dụng định tuyến dựa trên file, chúng ta có thể sử dụng máy chủ phát triển cục bộ của trang web để quản lý việc tự động tạo cấu hình route. Hãy khởi động máy chủ trang web cục bộ:
pnpm nx serve-local websiteyarn nx serve-local websitenpx nx serve-local websitebunx nx serve-local websiteBạn sẽ thấy một số lỗi, nhưng máy chủ trang web cục bộ sẽ khởi động trên cổng 4200, cũng như máy chủ Smithy API cục bộ trên cổng 3001.
Làm theo các bước dưới đây trong cả routes/index.tsx và routes/$shoppingListId.tsx để di chuyển sang TanStack Router:
-
Thêm
createFileRouteđể đăng ký mỗi route:import { createFileRoute } from "@tanstack/react-router";...export default ShoppingLists;export const Route = createFileRoute('/')({component: ShoppingLists,});import { createFileRoute } from "@tanstack/react-router";...export default ShoppingList;export const Route = createFileRoute('/$shoppingListId')({component: ShoppingList,});Sau khi bạn lưu file, bạn sẽ nhận thấy rằng các lỗi kiểu với lời gọi
createFileRouteđã biến mất. -
Thay thế hook
useNavigate.Cập nhật import:
import { useNavigate } from 'react-router-dom';import { useNavigate } from '@tanstack/react-router';Cập nhật các lời gọi đến phương thức
navigate(được trả về bởiuseNavigate) để truyền các route an toàn về kiểu:navigate(`/${cell.shoppingListId}`);navigate({to: '/$shoppingListId',params: { shoppingListId: cell.shoppingListId },}); -
Thay thế hook
useParams.Xóa import:
import { useParams } from 'react-router-dom';Cập nhật các lời gọi đến
useParamsvới hook được cung cấp bởiRouteđã tạo ở trên. Bây giờ chúng an toàn về kiểu!const { shoppingListId } = useParams();const { shoppingListId } = Route.useParams();
Sửa Import Component
Phần tiêu đề “Sửa Import Component”Vì các file route của chúng ta không còn lồng sâu trong cây thư mục như trong dự án PDK, chúng ta cần sửa import cho CreateItem trong cả routes/index.tsx và routes/$shoppingListId.tsx:
import CreateItem from "../../components/CreateItem";import CreateItem from "../components/CreateItem";AppLayoutContext cũng được cung cấp ở một vị trí hơi khác trong dự án mới của chúng ta:
import { AppLayoutContext } from "../../layouts/App";import { AppLayoutContext } from "../components/AppLayout";Di Chuyển để Sử Dụng Client TypeScript Được Tạo Mới
Phần tiêu đề “Di Chuyển để Sử Dụng Client TypeScript Được Tạo Mới”Chúng ta đang đến gần rồi! Tiếp theo, chúng ta cần di chuyển để sử dụng client TypeScript được cung cấp bởi Nx Plugin for AWS, có một số cải tiến so với Type Safe API. Để đạt được điều này, hãy làm theo các bước dưới đây
-
Import client và kiểu mới được tạo thay vì cái cũ, ví dụ:
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";Lưu ý rằng
routes/$shoppingListId.tsximport kiểuShoppingListdưới dạng_ShoppingList- trong file đó chúng ta nên làm tương tự, nhưng lại import từtypes.gen.Cũng lưu ý rằng chúng ta import các hook liên quan trực tiếp từ
@tanstack/react-query, vì client được tạo cung cấp các phương thức để tạo tùy chọn cho các hook TanStack query, thay vì các wrapper hook. -
Khởi tạo các hook TanStack Query mới, ví dụ:
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(),); -
Xóa wrapper
<operation>RequestContentcho các lời gọi đến các operation chấp nhận tham số trong request body:await putShoppingList.mutateAsync({putShoppingListRequestContent: {name: item,},});
Di Chuyển từ TanStack Query v4 sang v5
Phần tiêu đề “Di Chuyển từ TanStack Query v4 sang v5”Còn một vài lỗi cần sửa do sự khác biệt giữa TanStack Query v4 (được sử dụng bởi PDK) và v5 mà trình tạo api-connection đã thêm:
-
Thay thế
isLoadingbằngisPendingcho các mutation, ví dụ:putShoppingList.isLoadingputShoppingList.isPending -
Ứng dụng danh sách mua sắm sử dụng
InfiniteQueryTabletừ@aws-northstar/uimong đợi một kiểu từ TanStack Query v4. Điều này thực sự hoạt động với các infinite query từ v5, vì vậy chúng ta chỉ cần loại bỏ lỗi kiểu:<InfiniteQueryTablequery={getShoppingLists}query={getShoppingLists as any}
Truy Cập Trang Web Cục Bộ
Phần tiêu đề “Truy Cập Trang Web Cục Bộ”Bây giờ bạn có thể truy cập trang web cục bộ tại http://localhost:4200/
Trang web sẽ tải lên bây giờ sau khi mọi thứ đã được di chuyển! Vì cơ sở hạ tầng duy nhất mà ứng dụng danh sách mua sắm dựa vào ngoài API, Website và Identity là bảng DynamoDB - nếu bạn có bảng DynamoDB có tên shopping_list trong region, và thông tin xác thực AWS cục bộ có thể truy cập nó, trang web sẽ hoạt động đầy đủ!
Nếu không, không sao, chúng ta sẽ di chuyển cơ sở hạ tầng tiếp theo.
Di Chuyển Trang Shopping List
Trang Shopping Lists
/* 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,});Trang Shopping List
/* 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,});Di chuyển Infrastructure
Phần tiêu đề “Di chuyển Infrastructure”Dự án cuối cùng chúng ta cần di chuyển cho ứng dụng danh sách mua sắm của mình là InfrastructureTsProject. Đây là một dự án TypeScript CDK, mà tương đương với Nx Plugin for AWS là trình tạo ts#infra.
Cũng như các dự án Projen, PDK cũng cung cấp các CDK construct mà các dự án này phụ thuộc vào. Chúng ta sẽ di chuyển ứng dụng danh sách mua sắm khỏi các CDK construct này, thay vào đó sử dụng những construct được tạo bởi Nx Plugin for AWS.
Tạo một Dự án Hạ tầng TypeScript CDK
Phần tiêu đề “Tạo một Dự án Hạ tầng TypeScript CDK”Chạy trình tạo ts#infra để thiết lập dự án hạ tầng của bạn trong packages/infra:
- Install the Nx Console VSCode Plugin if you haven't already
- Open the Nx Console in VSCode
- Click
Generate (UI)in the "Common Nx Commands" section - Search for
@aws/nx-plugin - ts#infra - Fill in the required parameters
- name: infra
- Click
Generate
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-interactiveYou can also perform a dry-run to see what files would be changed
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-runDi chuyển Hạ tầng CDK
Phần tiêu đề “Di chuyển Hạ tầng CDK”Ứng dụng danh sách mua sắm PDK đã khởi tạo các construct sau trong CDK application stack:
DatabaseConstructcho bảng DynamoDB lưu trữ danh sách mua sắmUserIdentitycho các tài nguyên Cognito, được import trực tiếp từ PDKMyApiđể triển khai Smithy API, sử dụng TypeScript CDK construct được tạo ra với các tích hợp type-safe, phụ thuộc vào CDK constructTypeSafeRestApicủa PDK bên dưới.Websiteđể triển khai Website, bao bọc CDK constructStaticWebsitecủa PDK.
Tiếp theo, chúng ta sẽ di chuyển từng phần này sang dự án mới.
Sao chép Application Stack
Phần tiêu đề “Sao chép Application Stack”Sao chép packages/infra/src/stacks/application-stack.ts từ ứng dụng danh sách mua sắm PDK đến vị trí chính xác tương tự trong dự án mới của bạn. Bạn sẽ thấy một số lỗi TypeScript mà chúng ta sẽ xử lý bên dưới.
Sao chép Database Construct
Phần tiêu đề “Sao chép Database Construct”Ứng dụng danh sách mua sắm PDK có một construct Database trong packages/src/constructs/database.ts. Sao chép nó đến vị trí chính xác tương tự trong dự án mới của bạn.
Vì Nx Plugin for AWS sử dụng Checkov cho các bài kiểm tra bảo mật nghiêm ngặt hơn một chút so với PDK Nag, chúng ta cũng cần thêm một số suppression:
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',);Trong application-stack.ts, cập nhật import cho DatabaseConstruct để sử dụng cú pháp ESM:
import { DatabaseConstruct } from '../constructs/database';import { DatabaseConstruct } from '../constructs/database.js';Di chuyển UserIdentity Construct
Phần tiêu đề “Di chuyển UserIdentity Construct”Construct UserIdentity thường có thể được thay thế mà không cần thay đổi bằng cách điều chỉnh các import.
import { UserIdentity } from "@aws/pdk/identity";import { UserIdentity } from ':shopping-list/common-constructs';...const userIdentity = new UserIdentity(this, `${id}UserIdentity`);Lưu ý rằng các construct cơ bản được sử dụng bởi construct UserIdentity mới được cung cấp trực tiếp từ aws-cdk-lib, trong khi PDK sử dụng @aws-cdk/aws-cognito-identitypool-alpha.
Di chuyển API Construct
Phần tiêu đề “Di chuyển API Construct”Ứng dụng danh sách mua sắm PDK có một construct trong constructs/apis/myapi.ts khởi tạo một CDK construct mà Type Safe API đã tạo ra từ mô hình Smithy của bạn.
Cũng như construct này, vì dự án PDK sử dụng trait @handler, các CDK construct lambda function được tạo ra cũng được sinh ra.
Giống như Type Safe API, Nx Plugin for AWS cung cấp type-safety cho các tích hợp dựa trên mô hình Smithy của bạn, tuy nhiên nó được thực hiện theo cách đơn giản và linh hoạt hơn nhiều. Thay vì tạo ra toàn bộ một CDK construct tại thời điểm build, chỉ có “metadata” tối thiểu được tạo ra, mà packages/common/constructs/src/app/apis/api.ts sử dụng theo cách generic. Bạn có thể tìm hiểu thêm về cách sử dụng construct trong hướng dẫn trình tạo ts#smithy-api.
Làm theo các bước dưới đây:
-
Khởi tạo construct
Apitrongapplication-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(),});Lưu ý ở đây chúng ta sử dụng
Api.defaultIntegrations(this).build()- hành vi mặc định là tạo một lambda function cho mỗi operation trong API của chúng ta, đây là hành vi tương tự mà chúng ta đã có trongmyapi.ts. -
Cấp quyền cho các lambda function để truy cập bảng DynamoDB.
Trong ứng dụng danh sách mua sắm PDK,
DatabaseConsructđược truyền vàoMyApi, và nó quản lý việc thêm các quyền liên quan vào mỗi function construct được tạo ra. Chúng ta sẽ làm điều này trực tiếp trong fileapplication-stack.tsbằng cách truy cập thuộc tínhintegrationstype-safe của 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)); -
Cấp quyền cho người dùng đã xác thực để gọi API.
Trong
myapi.tscủa ứng dụng PDK, người dùng đã xác thực cũng được cấp quyền IAM để gọi API. Chúng ta sẽ làm tương đương trongapplication-stack.ts:stacks/application-stack.ts api.grantInvokeAccess(userIdentity.identityPool.authenticatedRole);
Di chuyển Website Construct
Phần tiêu đề “Di chuyển Website Construct”Cuối cùng, chúng ta thêm construct Website từ packages/common/constructs/src/app/static-websites/website.ts vào application-stack.ts, vì đây là tương đương với packages/infra/src/constructs/websites/website.ts của ứng dụng danh sách mua sắm PDK.
import { Website } from "../constructs/websites/website";import { Website } from ':shopping-list/common-constructs';...new Website(this, "Website", { userIdentity, myapi,});new Website(this, 'Website');Lưu ý rằng chúng ta không truyền identity hoặc API vào website - cấu hình runtime được quản lý trong mỗi construct được cung cấp bởi Nx Plugin for AWS, trong đó UserIdentity và Api đăng ký các giá trị cần thiết, và Website quản lý việc triển khai nó đến /runtime-config.json trên trang web tĩnh của bạn.
Bây giờ hãy build dự án sau khi chúng ta đã di chuyển tất cả các phần liên quan của codebase sang dự án mới.
pnpm nx run-many --target buildyarn nx run-many --target buildnpx nx run-many --target buildbunx nx run-many --target buildTriển khai
Phần tiêu đề “Triển khai”Bây giờ chúng ta đã có codebase được di chuyển hoàn toàn, chúng ta có thể xem xét việc triển khai nó. Có hai hướng đi mà chúng ta có thể thực hiện tại thời điểm này.
Tài Nguyên Hoàn Toàn Mới (Đơn Giản)
Phần tiêu đề “Tài Nguyên Hoàn Toàn Mới (Đơn Giản)”Cách tiếp cận đơn giản nhất là coi đây là một ứng dụng hoàn toàn mới, có nghĩa là chúng ta sẽ “bắt đầu lại” với một bảng DynamoDB mới và Cognito User Pool mới - mất tất cả người dùng và danh sách mua sắm của họ. Với cách tiếp cận này, chỉ cần:
-
Xóa bảng DynamoDB có tên
shopping_list -
Triển khai ứng dụng mới:
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/*
🎉 Và chúng ta đã hoàn thành! 🎉
Di Chuyển Các Tài Nguyên Stateful Hiện Có mà Không Có Thời Gian Ngừng Hoạt Động (Phức Tạp Hơn)
Phần tiêu đề “Di Chuyển Các Tài Nguyên Stateful Hiện Có mà Không Có Thời Gian Ngừng Hoạt Động (Phức Tạp Hơn)”Trên thực tế, nhiều khả năng bạn sẽ muốn di chuyển các tài nguyên AWS hiện có để chúng được quản lý bởi codebase mới, đồng thời tránh bất kỳ thời gian ngừng hoạt động nào cho khách hàng của bạn.
Đối với ứng dụng shopping list của chúng ta, các tài nguyên stateful mà chúng ta quan tâm là bảng DynamoDB chứa danh sách mua sắm của người dùng và User Pool chứa thông tin chi tiết của tất cả người dùng đã đăng ký. Kế hoạch tổng thể của chúng ta sẽ là giữ lại hai tài nguyên chính này và di chuyển chúng sao cho chúng được quản lý bởi stack mới của chúng ta, sau đó cập nhật DNS để trỏ đến website mới (và API nếu được expose cho khách hàng).
-
Cập nhật ứng dụng mới của bạn để tham chiếu các tài nguyên hiện có mà bạn muốn giữ lại.
Đối với ứng dụng shopping list, chúng ta thực hiện điều này cho bảng DynamoDB
constructs/database.ts this.shoppingListTable = new Table(this, 'ShoppingList', {...this.shoppingListTable = Table.fromTableName(this,'ShoppingList','shopping_list',);Và cho 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>',); -
Build và triển khai ứng dụng mới:
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/*Bây giờ chúng ta có ứng dụng mới đã được thiết lập tham chiếu các tài nguyên hiện có, chưa nhận bất kỳ traffic nào.
-
Thực hiện kiểm tra tích hợp đầy đủ để đảm bảo ứng dụng mới hoạt động như mong đợi. Đối với ứng dụng shopping list, tải website và kiểm tra xem bạn có thể đăng nhập và tạo, xem, chỉnh sửa và xóa danh sách mua sắm không.
-
Hoàn nguyên các thay đổi tham chiếu các tài nguyên hiện có trong ứng dụng mới của bạn, nhưng chưa triển khai chúng.
constructs/database.ts this.shoppingListTable = new Table(this, 'ShoppingList', {...this.shoppingListTable = Table.fromTableName(this,'ShoppingList','shopping_list',);Và cho 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>',);Và sau đó chạy build
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 -
Sử dụng
cdk importtrong thư mụcpackages/infracủa ứng dụng mới để xem các tài nguyên nào chúng ta sẽ được nhắc import.New Application cd packages/infrapnpm exec cdk import shopping-list-infra-sandbox/Application --forceBước qua các lời nhắc bằng cách nhấn enter. Import sẽ thất bại vì các tài nguyên được quản lý bởi một stack khác - điều này là mong đợi, chúng ta chỉ thực hiện bước này để xác nhận các tài nguyên nào chúng ta sẽ cần giữ lại. Bạn sẽ thấy output như thế này:
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) yĐiều này cho chúng ta biết rằng thực sự có 3 tài nguyên mà chúng ta sẽ cần import vào stack mới của mình.
-
Cập nhật dự án PDK cũ của bạn để đặt
RemovalPolicythànhRETAINcho các tài nguyên được phát hiện từ bước trước. Tại thời điểm viết bài này, đây là mặc định cho cả User Pool và bảng DynamoDB, nhưng chúng ta cần cập nhật nó cho SMS Role mà chúng ta đã phát hiện ở trên: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); -
Triển khai dự án PDK của bạn để các removal policy được áp dụng
PDK Application cd packages/infranpx projen deploy -
Xem console CloudFormation và ghi lại các giá trị mà bạn được nhắc trong bước
cdk importở trên- User Pool ID, ví dụ
us-west-2_XXXXX - SMS Role Name, ví dụ
infra-sandbox-UserIdentityUserPoolsmsRoleXXXXXX
- User Pool ID, ví dụ
-
Cập nhật dự án PDK của bạn để tham chiếu các tài nguyên hiện có thay vì tạo chúng
constructs/database.ts this.shoppingListTable = new Table(this, 'ShoppingList', {...this.shoppingListTable = Table.fromTableName(this,'ShoppingList','shopping_list',);Và cho 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,}); -
Triển khai lại dự án PDK của bạn, điều này có nghĩa là các tài nguyên không còn được quản lý bởi CloudFormation stack của dự án PDK của chúng ta.
PDK Application cd packages/infranpx projen deploy -
Bây giờ các tài nguyên không được quản lý, chúng ta có thể chạy
cdk importtrong ứng dụng mới của mình để thực sự thực hiện import:New Application cd packages/infrapnpm exec cdk import shopping-list-infra-sandbox/Application --forceNhập các giá trị khi được nhắc, import sẽ hoàn thành thành công.
-
Triển khai lại ứng dụng mới để đảm bảo rằng mọi thay đổi đối với các tài nguyên hiện có này (hiện được quản lý bởi stack mới của bạn) được thực hiệ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/* -
Thực hiện kiểm tra đầy đủ ứng dụng mới của bạn một lần nữa
-
Cập nhật các bản ghi DNS để trỏ đến Website mới của bạn (và API nếu cần).
Chúng tôi khuyến nghị một cách tiếp cận từng bước bằng cách sử dụng Route53 Weighted Routing, theo đó một phần nhỏ các request được chuyển đến ứng dụng mới để bắt đầu. Khi bạn theo dõi các metrics của mình, bạn có thể tăng trọng số cho ứng dụng mới cho đến khi không có traffic nào được gửi đến ứng dụng PDK cũ của bạn.
Nếu bạn không có DNS nào và đã sử dụng các domain được tạo tự động cho website và API, bạn luôn có thể xem xét việc proxy các request (ví dụ: thông qua CloudFront HTTP origin hoặc API Gateway HTTP integration(s)).
-
Theo dõi các metrics của ứng dụng PDK để đảm bảo không có traffic, và cuối cùng hủy CloudFormation stack cũ:
Terminal window cd packages/infranpx projen destroy
Điều đó phức tạp hơn khá nhiều, nhưng chúng ta đã di chuyển thành công người dùng của mình một cách liền mạch sang ứng dụng mới! 🎉🎉🎉
Bây giờ chúng ta có những lợi ích mới của Nx Plugin for AWS so với PDK:
- Build nhanh hơn
- Hỗ trợ phát triển API cục bộ
- Codebase thân thiện với vibe-coding (thử MCP server của chúng tôi!)
- Code client/server type-safe trực quan hơn
- Và nhiều hơn nữa!
Câu hỏi Thường gặp
Phần tiêu đề “Câu hỏi Thường gặp”Phần này cung cấp hướng dẫn cho các tính năng của PDK không được đề cập trong ví dụ di chuyển ở trên.
Theo quy tắc chung khi chuyển từ PDK, chúng tôi khuyên bạn nên bắt đầu bất kỳ dự án nào với một Nx Workspace, do sự tương đồng của nó với PDK Monorepo. Chúng tôi cũng khuyên bạn nên sử dụng các generator của chúng tôi làm các thành phần cơ bản để xây dựng bất kỳ loại mới nào.
npx create-nx-workspace@22.0.2 my-project --pm=pnpm --preset=@aws/nx-plugin --ci=skip --aiAgentsnpx create-nx-workspace@22.0.2 my-project --pm=yarn --preset=@aws/nx-plugin --ci=skip --aiAgentsnpx create-nx-workspace@22.0.2 my-project --pm=npm --preset=@aws/nx-plugin --ci=skip --aiAgentsnpx create-nx-workspace@22.0.2 my-project --pm=bun --preset=@aws/nx-plugin --ci=skip --aiAgentsCDK Graph
Phần tiêu đề “CDK Graph”CDK Graph xây dựng các biểu đồ về các tài nguyên CDK được kết nối của bạn, và cung cấp hai plugin:
Diagram Plugin
Phần tiêu đề “Diagram Plugin”CDK Graph Diagram Plugin tạo ra các sơ đồ kiến trúc AWS từ cơ sở hạ tầng CDK của bạn.
Để có một cách tiếp cận xác định tương tự, một lựa chọn khả thi là CDK-Dia.
Với những tiến bộ trong Generative AI, nhiều mô hình nền tảng có khả năng tạo ra các sơ đồ chất lượng cao từ cơ sở hạ tầng CDK của bạn. Chúng tôi khuyên bạn nên thử AWS Diagram MCP Server. Xem bài viết blog này để biết hướng dẫn chi tiết.
Threat Composer Plugin
Phần tiêu đề “Threat Composer Plugin”CDK Graph Threat Composer Plugin tạo ra một Threat Composer mô hình mối đe dọa khởi đầu từ mã CDK của bạn.
Plugin này hoạt động bằng cách đơn giản là lọc một mô hình mối đe dọa cơ sở chứa các mối đe dọa ví dụ, và lọc chúng dựa trên các tài nguyên mà stack của bạn sử dụng.
Nếu bạn quan tâm đến các mối đe dọa ví dụ cụ thể này, bạn có thể sao chép và lọc mô hình mối đe dọa cơ sở, hoặc sử dụng nó làm ngữ cảnh để giúp một mô hình nền tảng tạo ra một mô hình tương tự.
AWS Arch
Phần tiêu đề “AWS Arch”AWS Arch cung cấp các ánh xạ giữa các tài nguyên CloudFormation và các biểu tượng kiến trúc liên quan của chúng cho CDK Graph ở trên.
Tham khảo trang AWS Architecture Icons để biết các tài nguyên liên quan đến biểu tượng. Diagrams cũng cung cấp một cách để xây dựng sơ đồ dưới dạng mã.
Nếu bạn đang sử dụng trực tiếp dự án này, hãy cân nhắc fork dự án và tự quản lý nó!
Pipeline
Phần tiêu đề “Pipeline”PDK đã cung cấp một PDKPipelineProject để thiết lập một dự án cơ sở hạ tầng CDK và sử dụng một construct CDK bao bọc một số tài nguyên CDK Pipelines.
Để di chuyển từ cách này, bạn có thể sử dụng trực tiếp các construct CDK Pipelines. Tuy nhiên trên thực tế, có lẽ sẽ đơn giản hơn khi sử dụng các công cụ như GitHub actions hoặc GitLab CI/CD, nơi bạn định nghĩa các CDK Stages và chạy lệnh deploy cho stage phù hợp một cách trực tiếp.
PDK Nag
Phần tiêu đề “PDK Nag”PDK Nag bao bọc CDK Nag, và cung cấp một bộ quy tắc cụ thể để xây dựng các prototype.
Để di chuyển từ PDK Nag, hãy sử dụng CDK Nag trực tiếp. Nếu bạn cần cùng một bộ quy tắc, bạn có thể tạo một “pack” của riêng mình bằng cách làm theo tài liệu tại đây.
Type Safe API
Phần tiêu đề “Type Safe API”Các component được sử dụng phổ biến nhất từ Type Safe API đã được đề cập trong ví dụ di chuyển ở trên, tuy nhiên còn có các tính năng khác mà chi tiết di chuyển được nêu dưới đây.
API được Mô hình hóa với OpenAPI
Phần tiêu đề “API được Mô hình hóa với OpenAPI”Nx Plugin for AWS hỗ trợ các API được mô hình hóa bằng Smithy, nhưng không hỗ trợ những API được mô hình hóa trực tiếp bằng OpenAPI. Generator ts#smithy-api là một điểm khởi đầu tốt mà bạn có thể sau đó chỉnh sửa. Bạn có thể định nghĩa đặc tả OpenAPI của mình trong thư mục src của project model thay vì Smithy, và chỉnh sửa build.Dockerfile để sử dụng công cụ sinh code mong muốn cho clients/servers nếu chúng không có sẵn trên NPM. Nếu các công cụ mong muốn của bạn có trên NPM, bạn chỉ cần cài đặt chúng như dev dependencies vào Nx workspace của bạn và gọi chúng trực tiếp như các build target của Nx.
Backend
Phần tiêu đề “Backend”Đối với các backend type-safe được mô hình hóa bằng OpenAPI, bạn có thể cân nhắc sử dụng một trong các OpenAPI Generator Server Generators. Những công cụ này sẽ không sinh code trực tiếp cho AWS Lambda, nhưng bạn có thể sử dụng AWS Lambda Web Adapter để bắc cầu cho nhiều trong số chúng.
Client
Phần tiêu đề “Client”Đối với TypeScript clients, bạn có thể sử dụng generator ts#react-website và generator api-connection với một ví dụ ts#smithy-api để xem cách clients được sinh ra và tích hợp với một website. Điều này cấu hình các build target sinh clients bằng cách gọi các generator open-api#ts-client hoặc open-api#ts-hooks của chúng tôi. Bạn có thể tự sử dụng các generator này bằng cách trỏ chúng đến OpenAPI Specification của bạn.
Đối với các ngôn ngữ khác, bạn cũng có thể xem liệu có generator nào từ OpenAPI Generator phù hợp với nhu cầu của bạn không.
Bạn cũng có thể xây dựng một generator tùy chỉnh bằng cách sử dụng generator ts#nx-generator. Tham khảo tài liệu của generator đó để biết chi tiết về cách sinh code từ OpenAPI. Bạn có thể sử dụng các template từ Nx Plugin for AWS như một điểm khởi đầu. Bạn thậm chí có thể tham khảo các template từ codebase PDK để có thêm ý tưởng, lưu ý rằng cấu trúc dữ liệu mà các template hoạt động trên đó hơi khác so với Nx Plugin for AWS.
API được Mô hình hóa với TypeSpec
Phần tiêu đề “API được Mô hình hóa với TypeSpec”Đối với TypeSpec, phần OpenAPI ở trên cũng áp dụng tương tự. Bạn có thể bắt đầu bằng cách sinh một ts#smithy-api, cài đặt TypeSpec compiler và các OpenAPI packages vào Nx workspace của bạn, và cập nhật target compile của model project để chạy tsp compile thay thế, đảm bảo nó xuất ra một đặc tả OpenAPI vào thư mục dist.
Backend
Phần tiêu đề “Backend”Cách tiếp cận được khuyến nghị là sử dụng TypeSpec HTTP Server generator for JavaScript để sinh code server của bạn, vì điều này hoạt động trực tiếp trên mô hình TypeSpec của bạn.
Bạn có thể sử dụng AWS Lambda Web Adapter để chạy server được sinh ra trên AWS Lambda.
Bạn cũng có thể sử dụng bất kỳ tùy chọn OpenAPI nào ở trên.
Client
Phần tiêu đề “Client”TypeSpec có các code generator riêng cho clients trong cả ba ngôn ngữ được Type Safe API hỗ trợ:
Phần OpenAPI ở trên cũng áp dụng vì TypeSpec có thể compile sang OpenAPI.
API được Mô hình hóa với Smithy
Phần tiêu đề “API được Mô hình hóa với Smithy”Ví dụ di chuyển ở trên phác thảo việc di chuyển để sử dụng generator ts#smithy-api. Phần này đề cập đến các tùy chọn cho Python và Java backends và clients.
Backend
Phần tiêu đề “Backend”Smithy code generator for Java. Công cụ này có Java server generator cũng như một adapter để chạy Java server được sinh ra trên AWS Lambda.
Smithy không có server generator cho Python, vì vậy bạn sẽ cần đi qua OpenAPI. Tham khảo phần trên về API được Mô hình hóa với OpenAPI để biết các tùy chọn tiềm năng.
Client
Phần tiêu đề “Client”Smithy code generator for Java. Công cụ này có Java client generator.
Đối với Python clients, bạn có thể xem Smithy Python.
Đối với TypeScript, xem Smithy TypeScript, hoặc sử dụng cách tiếp cận tương tự mà chúng tôi đã thực hiện trong ts#smithy-api bằng cách đi qua OpenAPI (chúng tôi đã chọn cách này vì nó mang lại sự nhất quán giữa các API tRPC, FastAPI và Smithy thông qua TanStack Query hooks).
Smithy Shape Library
Phần tiêu đề “Smithy Shape Library”Type Safe API đã cung cấp một loại Projen project có tên SmithyShapeLibraryProject để cấu hình một project chứa các mô hình Smithy có thể được tái sử dụng bởi nhiều API dựa trên Smithy.
Cách đơn giản nhất để đạt được điều này là làm như sau:
Tạo một Shape Library
Phần tiêu đề “Tạo một Shape Library”-
Tạo shape library của bạn bằng generator
smithy#project:- Install the Nx Console VSCode Plugin if you haven't already
- Open the Nx Console in VSCode
- Click
Generate (UI)in the "Common Nx Commands" section - Search for
@aws/nx-plugin - smithy#project - Fill in the required parameters
- Click
Generate
Terminal window pnpm nx g @aws/nx-plugin:smithy#projectTerminal window yarn nx g @aws/nx-plugin:smithy#projectTerminal window npx nx g @aws/nx-plugin:smithy#projectTerminal window bunx nx g @aws/nx-plugin:smithy#projectYou can also perform a dry-run to see what files would be changed
Terminal window pnpm nx g @aws/nx-plugin:smithy#project --dry-runTerminal window yarn nx g @aws/nx-plugin:smithy#project --dry-runTerminal window npx nx g @aws/nx-plugin:smithy#project --dry-runTerminal window bunx nx g @aws/nx-plugin:smithy#project --dry-runChỉ định bất kỳ tên nào cho tùy chọn
serviceName, vì chúng ta sẽ xóa shapeservice. -
Thay thế mô hình mặc định trong
srcbằng các shape bạn muốn định nghĩa -
Cập nhật
smithy-build.jsonđể xóapluginsvà bất kỳ maven dependencies không sử dụng nào -
Thay thế
build.Dockerfilebằng các bước build tối thiểu:build.Dockerfile FROM public.ecr.aws/docker/library/node:24 AS builder# Output directoryRUN mkdir /out# Install Smithy CLI# https://smithy.io/2.0/guides/smithy-cli/cli_installation.htmlWORKDIR /smithyARG TARGETPLATFORMRUN if [ "$TARGETPLATFORM" = "linux/arm64" ]; then ARCH="aarch64"; else ARCH="x86_64"; fi && \mkdir -p smithy-install/smithy && \curl -L https://github.com/smithy-lang/smithy/releases/download/1.61.0/smithy-cli-linux-$ARCH.zip -o smithy-install/smithy-cli-linux-$ARCH.zip && \unzip -qo smithy-install/smithy-cli-linux-$ARCH.zip -d smithy-install && \mv smithy-install/smithy-cli-linux-$ARCH/* smithy-install/smithyRUN smithy-install/smithy/install# Copy project filesCOPY smithy-build.json .COPY src src# Smithy build with Maven cache mountRUN --mount=type=cache,target=/root/.m2/repository,id=maven-cache \smithy buildRUN cp -r build/* /out/# Export the /out directoryFROM scratch AS exportCOPY --from=builder /out /
Sử dụng Shape Library
Phần tiêu đề “Sử dụng Shape Library”Trong (các) service model project của bạn, thực hiện các thay đổi sau để sử dụng shape library:
-
Cập nhật target
compiletrongproject.jsonđể thêm workspace làm build context, và một dependency vào targetbuildcủa shape libraryproject.json {"cache": true,"outputs": ["{workspaceRoot}/dist/{projectRoot}/build"],"executor": "nx:run-commands","options": {"commands": ["rimraf dist/packages/api/model/build","make-dir dist/packages/api/model/build","docker build --build-context workspace=. -f packages/api/model/build.Dockerfile --target export --output type=local,dest=dist/packages/api/model/build packages/api/model"],"parallel": false,"cwd": "{workspaceRoot}"},"dependsOn": ["@my-project/shapes:build"]} -
Cập nhật
build.Dockerfileđể sao chép thư mụcsrctừ shape library của bạn. Ví dụ, giả sử shape library nằm trongpackages/shapes:build.Dockerfile # Copy project filesCOPY smithy-build.json .COPY src srcCOPY --from=workspace packages/shapes/src shapes -
Cập nhật
smithy-build.jsonđể thêm thư mục shapes vàosources:smithy-build.json {"version": "1.0","sources": ["src/", "shapes/"],"plugins": {...}
Interceptors
Phần tiêu đề “Interceptors”Type Safe API đã cung cấp các interceptor mặc định sau:
- Logging, tracing và metrics interceptors sử dụng Powertools for AWS Lambda
- Try-catch interceptor để xử lý các exception chưa được bắt
- CORS interceptor để trả về CORS headers
Generator ts#smithy-api tích hợp logging, tracing và metrics với Powertools for AWS Lambda sử dụng Middy. Hành vi của try-catch interceptor được tích hợp sẵn trong Smithy TypeScript SSDK, và CORS headers được thêm vào trong handler.ts.
Đối với logging, tracing và metrics interceptors trong bất kỳ ngôn ngữ nào, sử dụng Powertools for AWS Lambda trực tiếp.
Để di chuyển các custom interceptors, chúng tôi khuyến nghị sử dụng các thư viện sau:
- TypeScript - Middy
- Python - Powertools for AWS Lambda Middleware Factory
- Java - Tích hợp các phương thức trước/sau logic nghiệp vụ của bạn sử dụng aws-lambda-java-libs cho một cách tiếp cận đơn giản, hoặc cân nhắc AspectJ để xây dựng middleware của bạn như các annotation.
Sinh Tài liệu
Phần tiêu đề “Sinh Tài liệu”Type Safe API đã cung cấp sinh tài liệu sử dụng Redocly CLI. Điều này rất dễ thêm vào một project hiện có sau khi bạn đã di chuyển nó như trên.
-
Cài đặt 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 install -D @redocly/cli -
Thêm một target sinh tài liệu vào project
modelcủa bạn sử dụngredocly build-docs, ví dụ: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"]}}
Bạn cũng có thể cân nhắc các OpenAPI Generator documentation generators.
Mock Integrations
Phần tiêu đề “Mock Integrations”Type Safe API đã sinh các mock cho bạn trong infrastructure package được sinh ra của nó.
Bạn có thể chuyển sang JSON Schema Faker có thể tạo dữ liệu mock dựa trên JSON Schemas. Công cụ này có thể hoạt động trực tiếp trên một đặc tả OpenAPI, và có một CLI mà bạn có thể chạy như một phần của quá trình build project model của bạn.
Bạn có thể cập nhật CDK infrastructure của mình để đọc file JSON được xuất ra bởi JSON Schema Faker, và trả về API Gateway MockIntegration phù hợp cho một integration, dựa trên metadata.gen.ts được sinh ra (giả sử bạn đã sử dụng generator ts#smithy-api).
Backend Đa Ngôn ngữ
Phần tiêu đề “Backend Đa Ngôn ngữ”Type Safe API đã hỗ trợ triển khai API với sự kết hợp của các ngôn ngữ khác nhau trong backend. Điều này cũng có thể đạt được bằng cách cung cấp “overrides” cho các integration khi khởi tạo API construct của bạn trong 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(),});Bạn sẽ cần “stub” service/router của mình để service của bạn có thể compile nếu sử dụng ts#smithy-api và TypeScript Server SDK, ví dụ:
export const Service: ApiService<ServiceContext> = { ... Echo: () => { throw new Error(`Not Implemented`); },};Xác thực Đầu vào
Phần tiêu đề “Xác thực Đầu vào”Type Safe API đã thêm xác thực API Gateway native cho request bodies dựa trên đặc tả OpenAPI của bạn vì nó sử dụng construct SpecRestApi bên dưới.
Với generator ts#smithy-api, xác thực được thực hiện bởi chính Server SDK. Điều này tương tự đối với hầu hết các server generator.
Nếu bạn muốn triển khai xác thực API Gateway native, bạn có thể làm như vậy bằng cách chỉnh sửa packages/common/constructs/src/core/api/rest-api.ts để đọc JSON schema liên quan cho request body của mỗi operation từ đặc tả OpenAPI của bạn.
WebSocket APIs
Phần tiêu đề “WebSocket APIs”Thật không may, không có con đường di chuyển đơn giản nào cho websocket API của Type Safe API sử dụng API Gateway và Lambda với phát triển API dựa trên mô hình. Tuy nhiên, phần này của hướng dẫn nhằm mục đích ít nhất cung cấp một vài ý tưởng.
Cân nhắc sử dụng AsyncAPI để mô hình hóa API của bạn thay vì OpenAPI hoặc TypeSpec vì điều này được thiết kế để xử lý các API bất đồng bộ. AsyncAPI NodeJS Template có thể sinh một Node websocket backend mà bạn có thể host trên ECS chẳng hạn.
Bạn cũng có thể cân nhắc AppSync Events cho infrastructure, và sử dụng Powertools. Bài đăng blog này đáng đọc!
Một tùy chọn khác là sử dụng GraphQL APIs với websockets trên AppSync, mà chúng tôi có một GitHub issue mà bạn có thể +1! Tham khảo hướng dẫn dành cho nhà phát triển AppSync để biết chi tiết và liên kết đến các dự án mẫu.
Bạn cũng có thể cân nhắc tự xây dựng các code generator của riêng mình để diễn giải các vendor extension giống như Type Safe API. Tham khảo phần API được Mô hình hóa với OpenAPI để biết chi tiết về việc xây dựng các code generator tùy chỉnh dựa trên OpenAPI. Bạn có thể tìm thấy các template mà Type Safe API sử dụng cho API Gateway Websocket API Lambda handlers tại đây, và client tại đây.
Bạn cũng có thể cân nhắc di chuyển để sử dụng generator ts#trpc-api để sử dụng tRPC. Tại thời điểm viết bài, chúng tôi chưa có hỗ trợ cho subscriptions/streaming nhưng nếu đây là điều bạn cần, hãy thêm +1 vào GitHub issue theo dõi điều này.
Smithy là protocol agnostic, nhưng chưa có hỗ trợ cho giao thức Websocket, tham khảo GitHub issue này theo dõi hỗ trợ.
Infrastructure bằng Python hoặc Java
Phần tiêu đề “Infrastructure bằng Python hoặc Java”PDK hỗ trợ cơ sở hạ tầng CDK được viết bằng Python và Java. Chúng tôi không hỗ trợ điều này trong Nx Plugin for AWS tại thời điểm viết bài.
Hướng đi được khuyến nghị là di chuyển cơ sở hạ tầng CDK của bạn sang TypeScript, hoặc sử dụng các generator của chúng tôi và di chuyển gói common constructs sang ngôn ngữ mong muốn của bạn. Bạn có thể sử dụng Generative AI để tăng tốc các loại di chuyển này, ví dụ như Amazon Q CLI. Bạn có thể để một AI agent lặp lại quá trình di chuyển cho đến khi các template CloudFormation được tổng hợp giống hệt nhau.
Điều tương tự áp dụng cho cơ sở hạ tầng được tạo ra bởi Type Safe API trong Python hoặc Java - bạn có thể dịch construct rest-api.ts chung từ gói common constructs, và triển khai metadata generator đơn giản của riêng bạn cho ngôn ngữ đích (tham khảo phần APIs Modelled with OpenAPI).
Bạn có thể sử dụng generator py#project cho một dự án Python cơ bản để thêm mã CDK của bạn vào (và di chuyển file cdk.json của bạn, thêm các target liên quan). Bạn có thể sử dụng plugin @nx/gradle của Nx cho các dự án Java, hoặc @jnxplus/nx-maven cho Maven.
Sử dụng Projen
Phần tiêu đề “Sử dụng Projen”PDK được xây dựng dựa trên Projen. Projen và Nx Generators có những khác biệt khá cơ bản, điều này có nghĩa là mặc dù về mặt kỹ thuật có thể kết hợp chúng nhưng rất có thể đây là một anti-pattern. Projen quản lý các file dự án dưới dạng code sao cho chúng không thể được chỉnh sửa trực tiếp, trong khi Nx generators tạo ra các file dự án một lần và sau đó code có thể được tự do chỉnh sửa.
Nếu bạn muốn tiếp tục sử dụng Projen, bạn có thể tự triển khai các loại dự án Projen mong muốn của mình. Để tuân theo các pattern từ Nx Plugin for AWS, bạn có thể chạy các generators của chúng tôi hoặc xem xét mã nguồn của chúng trên GitHub để thấy cách các loại dự án mong muốn của bạn được xây dựng, và triển khai các phần liên quan bằng cách sử dụng các primitives của Projen.