Di chuyển từ AWS PDK
Hướng dẫn này sẽ đưa bạn qua một ví dụ di chuyển dự án AWS PDK sang Nx Plugin for AWS, cũng như 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:
- Build 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 nhiều 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ừ PDK Tutorial 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ự 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 chúng ta. 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:
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=cdkMở 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 đã sử dụng:
- Smithy làm ngôn ngữ mô hình hóa
- TypeScript để triển khai các thao tác
- Tạo hook TypeScript để tích hợp với trang web 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 TypeScript Smithy API
Phần tiêu đề “Tạo TypeScript Smithy API”Chạy generator ts#api với framework được đặt thành smithy để thiết lập dự án api của bạn trong 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-interactiveBạn cũng có thể thực hiện chạy thử để xem những tệp nào sẽ bị thay đổi
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- Cài đặt Nx Console VSCode Plugin nếu bạn chưa cài đặt
- Mở Nx Console trong VSCode
- Nhấp
Generate (UI)trong phần "Common Nx Commands" - Tìm kiếm
@aws/nx-plugin - ts#api - Điền các tham số bắt buộc
- name: api
- framework: smithy
- namespace: com.aws
- auth: iam
- Nhấp
Generate
Bạ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 thao tácGetShoppingListstrongget-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ỳ thao tác
@paginatednào cũng nên sử dụng@cursornếu bạn đang sử dụng generator client đượ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 thao tác vì điều này không được hỗ trợ bởi Nx Plugin for AWS. Sử dụngts#smithy-api, chúng ta không cần các cấu trúc CDK hàm lambda được tạo tự động và các mục tiêu đó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 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 Handlers
Phần tiêu đề “Di chuyển Lambda Handlers”Bạn có thể coi dự án api/backend 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 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 gói @aws-sdk/client-dynamodb, vì vậy hãy cài đặt nó vào dự án @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/apiSau đó, hãy sao chép tệp handlers/src/dynamo-client.ts từ dự án PDK sang backend/src/operations để nó có sẵn cho các handler của chúng ta.
Generator ts#smithy-api tạo ra một thao tác Echo ví dụ. Vì chúng ta đã xóa điều này khỏi mô hình của mình, hãy xóa handler tương ứng trong backend/src/operations/echo.ts. Chúng ta sẽ đăng ký các thao tác đã di chuyển của mình trong service.ts bên dưới.
Để 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 loại thao tác 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 handler thao tác 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 loại 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 bất kỳ tham chiếu đầu vào nào cho phù hợp:
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 bất kỳ import nào để 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 thao tác 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 để 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ỳ trường hợp nào của getApiServiceHandler bằng 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 bằng Build
Phần tiêu đề “Xác minh bằng Build”Bây giờ chúng ta có thể build dự án để kiểm tra xem quá trình di chuyển đã hoạt động cho đến nay chưa:
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 website 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 website trong hướng dẫn này, chúng ta sẽ sử dụng generator ts#website, sử dụng các công nghệ hiện đại và được hỗ trợ hơn, cụ thể là Vite.
Là một phần của 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, cung cấp thêm tính an toàn kiểu cho định tuyến website.
Tạo một React Website
Phần tiêu đề “Tạo một React Website”Chạy generator ts#website với framework được đặt thành react để thiết lập dự án website của bạn trong packages/website. Vì ứng dụng danh sách mua sắm được xây dựng với các component CloudScape, chúng ta cũng đặt ux thành cloudscape (mặc định là 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-interactiveBạn cũng có thể thực hiện chạy thử để xem những tệp nào sẽ bị thay đổi
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- Cài đặt Nx Console VSCode Plugin nếu bạn chưa cài đặt
- Mở Nx Console trong VSCode
- Nhấp
Generate (UI)trong phần "Common Nx Commands" - Tìm kiếm
@aws/nx-plugin - ts#website - Điền các tham số bắt buộc
- name: website
- framework: react
- ux: cloudscape
- Nhấp
Generate
Thêm Xác thực Cognito
Phần tiêu đề “Thêm Xác thực Cognito”Generator React website ở trên không tích hợp sẵn xác thực cognito như CloudscapeReactTsWebsiteProject, thay vào đó nó được thêm một cách rõ ràng thông qua generator 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-interactiveBạn cũng có thể thực hiện chạy thử để xem những tệp nào sẽ bị thay đổi
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- Cài đặt Nx Console VSCode Plugin nếu bạn chưa cài đặt
- Mở Nx Console trong VSCode
- Nhấp
Generate (UI)trong phần "Common Nx Commands" - Tìm kiếm
@aws/nx-plugin - ts#website#auth - Điền các tham số bắt buộc
- project: website
- cognitoDomain: shopping-list
- Nhấp
Generate
Điều này thêm các component React quản lý các chuyển hướng thích hợp để đảm bảo người dùng đăng nhập bằng giao diện Cognito hosted UI. Đ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 Website với API
Phần tiêu đề “Kết nối Website 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 website 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 generator connection. Tiếp theo, chúng ta sử dụng generator này để website của chúng ta có thể gọi Smithy API của chúng ta:
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-interactiveBạn cũng có thể thực hiện chạy thử để xem những tệp nào sẽ bị thay đổi
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- Cài đặt Nx Console VSCode Plugin nếu bạn chưa cài đặt
- Mở Nx Console trong VSCode
- Nhấp
Generate (UI)trong phần "Common Nx Commands" - Tìm kiếm
@aws/nx-plugin - connection - Điền các tham số bắt buộc
- sourceProject: website
- targetProject: api
- Nhấp
Generate
Điều này tạo ra các provider client cần thiết và các target build để website của bạn có thể gọi API của bạn thông qua một client TypeScript được tạo ra.
Thêm Dependency AWS Northstar
Phần tiêu đề “Thêm Dependency AWS Northstar”CloudscapeReactTsWebsiteProject tự động bao gồm một dependency trên @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ó vào dự án @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 tích hợp một component code editor phụ thuộc vào ace-builds, sử dụng một import đặc thù của webpack mà Vite không thể phân giải. Vì ứng dụng danh sách mua sắm của chúng ta không sử dụng component này, chúng ta loại trừ nó khỏi bundle bằng cách thêm nó vào cấu hình external trong các tùy chọn build hiện có trong packages/website/vite.config.mts:
build: { outDir: '../../dist/packages/website/bundle', emptyOutDir: true, reportCompressedSize: true, commonjsOptions: { transformMixedEsModules: true, }, rollupOptions: { external: ['ace-builds/webpack-resolver'], }, },Di chuyển Components và Pages
Phần tiêu đề “Di chuyển Components và Pages”Ứng dụng danh sách mua sắm có một component gọi là CreateItem, và hai page, ShoppingList và ShoppingLists. Chúng ta sẽ di chuyển chúng sang website 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 bạn, 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 file-based routing, chúng ta có thể sử dụng máy chủ phát triển website cục bộ để quản lý việc tự động tạo cấu hình route.
Hãy khởi động máy chủ website cục bộ:
pnpm nx dev websiteyarn nx dev websitenpx nx dev websitebunx nx dev websiteBạn sẽ thấy một số lỗi, nhưng máy chủ website 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 đến
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 vào các route an toàn 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được tạo ở trên. Bây giờ chúng đã an toàn kiểu!const { shoppingListId } = useParams();const { shoppingListId } = Route.useParams();
Sửa Component Imports
Phần tiêu đề “Sửa Component Imports”Vì các file route của chúng ta không được lồng sâu trong cây file như trong dự án PDK của chúng ta, 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 Generated TypeScript Client mới
Phần tiêu đề “Di chuyển để sử dụng Generated TypeScript Client mới”Chúng ta đang gần hoàn thành 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à types được tạo mới 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 typeShoppingListdướ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 ra cung cấp các phương thức để tạo options 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à generator 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 type 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 bỏ qua lỗi kiểu:<InfiniteQueryTablequery={getShoppingLists}query={getShoppingLists as any}
Truy cập Website Cục bộ
Phần tiêu đề “Truy cập Website Cục bộ”Bây giờ bạn có thể truy cập website cục bộ tại http://localhost:4200/
Website sẽ tải lên bây giờ 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ó một bảng DynamoDB có tên shopping_list trong vùng, và thông tin xác thực AWS cục bộ có thể truy cập nó, website 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 Shopping List Page
Shopping Lists Page
/* 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,});Shopping List Page
/* 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 là InfrastructureTsProject. Đây là một dự án TypeScript CDK, mà tương đương trong 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 construct CDK 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 construct CDK 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 Cơ sở hạ tầng TypeScript CDK
Phần tiêu đề “Tạo một Dự án Cơ sở hạ tầng TypeScript CDK”Chạy trình tạo ts#infra để thiết lập dự án cơ sở hạ tầng của bạn trong 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-interactiveBạn cũng có thể thực hiện chạy thử để xem những tệp nào sẽ bị thay đổi
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- Cài đặt Nx Console VSCode Plugin nếu bạn chưa cài đặt
- Mở Nx Console trong VSCode
- Nhấp
Generate (UI)trong phần "Common Nx Commands" - Tìm kiếm
@aws/nx-plugin - ts#infra - Điền các tham số bắt buộc
- name: infra
- Nhấp
Generate
Di chuyển Cơ sở hạ tầng CDK
Phần tiêu đề “Di chuyển Cơ sở hạ tầng CDK”Ứng dụng danh sách mua sắm PDK đã khởi tạo các construct sau trong stack ứng dụng CDK:
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 construct TypeScript CDK được tạo với các tích hợp type-safe, phụ thuộc vào construct CDKTypeSafeRestApicủa PDK bên dưới.Websiteđể triển khai Website, bao bọc construct CDKStaticWebsitecủa PDK.
Tiếp theo, chúng ta sẽ di chuyển từng cái trong số 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 chính xác cùng vị trí 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ẽ giải quyết 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 chính xác cùng vị trí 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 construct CDK 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 construct CDK hàm lambda được tạo 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 toàn bộ construct CDK 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 chung. 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.
Thực hiện các bước sau:
-
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 hàm lambda cho mỗi operation trong API của chúng ta, đây là hành vi giống như chúng ta có trongmyapi.ts. -
Cấp quyền cho các hàm lambda 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 construct hàm được tạo. 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 cho 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.
Hãy build dự án bây giờ 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 buildDeploy
Phần tiêu đề “Deploy”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ất cả Tài nguyên Mới (Đơn giản)
Phần tiêu đề “Tất cả Tài nguyê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 Tài nguyên Stateful Hiện có mà không có Downtime (Phức tạp hơn)
Phần tiêu đề “Di chuyển Tài nguyên Stateful Hiện có mà không có Downtime (Phức tạp hơn)”Trong 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 của chúng ta (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 làm đ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 thử 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 bạn có thể đăng nhập và tạo, xem, chỉnh sửa và xóa danh sách mua sắm.
-
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 prompt 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 chúng ta.
-
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 dự án PDK của bạn một lần nữa, đ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 chúng ta để 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 ứng dụng mới một lần nữa để đảm bảo rằng bất kỳ thay đổi nào đố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 thử đầ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ị cách tiếp cận dần dần 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 metric 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ó bất kỳ DNS nào và sử dụng các domain tự động tạo 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 metric 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 một chút, 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!
Frequently Asked Questions
Phần tiêu đề “Frequently Asked Questions”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.
Như một quy tắc chung khi chuyển từ PDK, chúng tôi khuyến nghị 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 nghị sử dụng các generator của chúng tôi làm nền tảng để xây dựng bất kỳ loại mới nào.
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
Phần tiêu đề “CDK Graph”CDK Graph xây dựng đồ thị 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 giải pháp thay thế 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 đăng 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 mô hình mối đe dọa Threat Composer khởi đầu từ mã CDK của bạn.
Plugin này hoạt động bằng cách đơn giản 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 á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 cách để xây dựng sơ đồ dưới dạng mã.
Nếu bạn đang sử dụng trực tiếp điều này, hãy cân nhắc fork dự án và tự quản lý!
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 trong thực tế, có thể đơ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 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 tập hợp các quy tắc cụ thể để xây dựng nguyên mẫu.
Để 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 tập hợp 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 thành phần đượ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, với chi tiết di chuyển được nêu dưới đây.
API được Mô hình hóa bằng OpenAPI
Phần tiêu đề “API được Mô hình hóa bằng OpenAPI”Nx Plugin for AWS hỗ trợ các API được mô hình hóa trong 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ể sửa đổi sau đó. Bạn có thể định nghĩa đặc tả OpenAPI của mình trong thư mục src của dự án model thay vì Smithy, và sửa đổi build.Dockerfile để sử dụng công cụ tạo mã 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 Nx build targets.
Backend
Phần tiêu đề “Backend”Đối với các backend type-safe được mô hình hóa trong 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 tạo trực tiếp cho AWS Lambda, nhưng bạn có thể sử dụng AWS Lambda Web Adapter để kết nối 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#website và generator connection với một ví dụ ts#api (với framework được đặt thành smithy) để xem cách clients được tạo và tích hợp với một website. Điều này cấu hình các build targets tạo 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 tạo mã từ OpenAPI. Bạn có thể sử dụng các template từ Nx Plugin for AWS làm điểm khởi đầu. Bạn thậm chí còn có thể tham khảo các template từ codebase PDK để có thêm cảm hứ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 bằng TypeSpec
Phần tiêu đề “API được Mô hình hóa bằng TypeSpec”Đối với TypeSpec, phần trên về OpenAPI cũng áp dụng. Bạn có thể bắt đầu bằng cách tạo một ts#smithy-api, cài đặt TypeSpec compiler và các gói OpenAPI vào Nx workspace của bạn, và cập nhật target compile của dự án model để 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 để tạo mã 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 tạo 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ể biên dịch sang OpenAPI.
API được Mô hình hóa bằng Smithy
Phần tiêu đề “API được Mô hình hóa bằng 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 tạo trên AWS Lambda.
Smithy không có server generator cho Python, vì vậy bạn sẽ cần phải đi qua OpenAPI. Tham khảo phần trên về API được Mô hình hóa bằng 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, hãy 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 dự án Projen có tên SmithyShapeLibraryProject cấu hình một dự án 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.
Tương đương là generator smithy#project với type được đặt thành 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=shapesBạn cũng có thể thực hiện chạy thử để xem những tệp nào sẽ bị thay đổi
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- Cài đặt Nx Console VSCode Plugin nếu bạn chưa cài đặt
- Mở Nx Console trong VSCode
- Nhấp
Generate (UI)trong phần "Common Nx Commands" - Tìm kiếm
@aws/nx-plugin - smithy#project - Điền các tham số bắt buộc
- name: my-shapes
- type: shapes
- Nhấp
Generate
Di chuyển các shapes từ SmithyShapeLibraryProject của bạn vào thư mục src của dự án được tạo, sau đó tham khảo hướng dẫn dự án Smithy để biết cách kết nối thư viện như một dependency của model API của bạn.
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 vào 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, hãy sử dụng Powertools for AWS Lambda trực tiếp.
Để di chuyển các interceptor tùy chỉnh, 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 bằng cách 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 dưới dạng annotations.
Tạo Tài liệu
Phần tiêu đề “Tạo Tài liệu”Type Safe API cung cấp tạo tài liệu sử dụng Redocly CLI. Điều này rất dễ thêm vào một dự án 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 add -D @redocly/cli -
Thêm một target tạo tài liệu vào dự án
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 tạo mocks cho bạn trong gói infrastructure được tạo 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. Điều 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 dự án model của bạn.
Bạn có thể cập nhật cơ sở hạ tầng CDK của mình để đọc tệp JSON được xuất ra bởi JSON Schema Faker, và trả về MockIntegration API Gateway phù hợp cho một integration, dựa trên metadata.gen.ts được tạo (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 integrations 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 bạn để service của bạn biên dịch 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 gốc 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 giống nhau đối với hầu hết các server generators.
Nếu bạn muốn triển khai xác thực API Gateway gốc, bạn có thể làm như vậy bằng cách sửa đổi 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ó đường dẫn di chuyển đơn giản 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ể tạo 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 cơ sở hạ tầng, 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 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 generators của riêng bạn để diễn giải các vendor extensions giống như Type Safe API. Tham khảo phần API được Mô hình hóa bằng 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 này, 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 của chúng tôi.
Smithy là protocol agnostic, nhưng chưa có hỗ trợ cho giao thức Websocket, tham khảo GitHub issue theo dõi hỗ trợ này.
Infrastructure in Python or Java
Phần tiêu đề “Infrastructure in Python or Java”PDK hỗ trợ CDK infrastructure đượ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 này.
Hướng đi được khuyến nghị là di chuyển CDK infrastructure 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ư Kiro CLI. Bạn có thể để một AI agent lặp lại quá trình di chuyển cho đến khi các CloudFormation template được tổng hợp giống hệt nhau.
Điều tương tự áp dụng cho infrastructure đượ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 tệp 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.
Use of Projen
Phần tiêu đề “Use of Projen”PDK được xây dựng trên nền tảng Projen. Projen và Nx Generators có những khác biệt khá cơ bản, nghĩa là mặc dù về mặt kỹ thuật có thể kết hợp chúng nhưng điều đó có thể là một anti-pattern. Projen quản lý các tệp dự án dưới dạng mã sao cho chúng không thể được sửa đổi trực tiếp, trong khi Nx generators tạo ra các tệp dự án một lần và sau đó mã có thể được tự do sửa đổi.
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 mẫu từ Nx Plugin for AWS, bạn có thể chạy các generators của chúng tôi hoặc kiểm tra mã nguồn của chúng trên GitHub để xem 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 nguyên thủy của Projen.