Bỏ qua để đến nội dung

API TypeScript của Smithy

Smithy là một ngôn ngữ định nghĩa giao diện độc lập với giao thức để tạo API theo cách hướng mô hình.

Trình tạo Smithy TypeScript API tạo một API mới sử dụng Smithy để định nghĩa dịch vụ, và Smithy TypeScript Server SDK để triển khai. Trình tạo cung cấp infrastructure as code bằng CDK hoặc Terraform để triển khai dịch vụ của bạn lên AWS Lambda, được truy cập thông qua AWS API Gateway REST API. Nó cung cấp phát triển API an toàn kiểu với tự động tạo mã từ mô hình Smithy. Handler được tạo sử dụng AWS Lambda Powertools for TypeScript cho khả năng quan sát, bao gồm logging, AWS X-Ray tracing và CloudWatch Metrics

Bạn có thể tạo một Smithy TypeScript API mới theo hai cách:

  1. Install the Nx Console VSCode Plugin if you haven't already
  2. Open the Nx Console in VSCode
  3. Click Generate (UI) in the "Common Nx Commands" section
  4. Search for @aws/nx-plugin - ts#smithy-api
  5. Fill in the required parameters
    • Click Generate
    Parameter Type Default Description
    name Required string - The name of the API (required). Used to generate class names and file paths.
    namespace string - The namespace for the Smithy API. Defaults to your monorepo scope
    computeType string ServerlessApiGatewayRestApi The type of compute to use to deploy this API.
    auth string IAM The method used to authenticate with your API. Choose between IAM (default), Cognito or None.
    directory string packages The directory to store the application in.
    iacProvider string Inherit The preferred IaC provider. By default this is inherited from your initial selection.

    Trình tạo tạo hai dự án liên quan trong thư mục <directory>/<api-name>:

    • Thư mụcmodel/ Dự án mô hình Smithy
      • project.json Cấu hình dự án và build targets
      • smithy-build.json Cấu hình build Smithy
      • build.Dockerfile Cấu hình Docker để build các artifact Smithy
      • Thư mụcsrc/
        • main.smithy Định nghĩa dịch vụ chính
        • Thư mụcoperations/
          • echo.smithy Ví dụ định nghĩa operation
    • Thư mụcbackend/ Triển khai backend TypeScript
      • project.json Cấu hình dự án và build targets
      • rolldown.config.ts Cấu hình bundle
      • Thư mụcsrc/
        • handler.ts AWS Lambda handler
        • local-server.ts Server phát triển local
        • service.ts Triển khai dịch vụ
        • context.ts Định nghĩa context dịch vụ
        • Thư mụcoperations/
          • echo.ts Ví dụ triển khai operation
        • Thư mụcgenerated/ TypeScript SDK được tạo (tạo trong quá trình build)

    Vì trình tạo này tạo infrastructure as code dựa trên iacProvider bạn chọn, nó sẽ tạo một dự án trong packages/common bao gồm các CDK constructs hoặc Terraform modules tương ứng.

    Dự án infrastructure as code chung được cấu trúc như sau:

    • Thư mụcpackages/common/constructs
      • Thư mụcsrc
        • Thư mụcapp/ Constructs cho infrastructure cụ thể cho một dự án/trình tạo
          • Thư mụcapis/
            • <project-name>.ts CDK construct để triển khai API của bạn
        • Thư mụccore/ Constructs chung được tái sử dụng bởi constructs trong app
          • Thư mụcapi/
            • rest-api.ts CDK construct để triển khai REST API
            • utils.ts Tiện ích cho API constructs
        • index.ts Entry point xuất các constructs từ app
      • project.json Build targets và cấu hình dự án

    Operations được định nghĩa trong các file Smithy trong dự án model. Định nghĩa dịch vụ chính nằm trong main.smithy:

    $version: "2.0"
    namespace your.namespace
    use aws.protocols#restJson1
    use smithy.framework#ValidationException
    @title("YourService")
    @restJson1
    service YourService {
    version: "1.0.0"
    operations: [
    Echo,
    // Thêm operations của bạn ở đây
    ]
    errors: [
    ValidationException
    ]
    }

    Các operations riêng lẻ được định nghĩa trong các file riêng biệt trong thư mục operations/:

    $version: "2.0"
    namespace your.namespace
    @http(method: "POST", uri: "/echo")
    operation Echo {
    input: EchoInput
    output: EchoOutput
    }
    structure EchoInput {
    @required
    message: String
    foo: Integer
    bar: String
    }
    structure EchoOutput {
    @required
    message: String
    }

    Các triển khai operation nằm trong thư mục src/operations/ của dự án backend. Mỗi operation được triển khai sử dụng các kiểu được tạo từ TypeScript Server SDK (được tạo tại thời điểm build từ mô hình Smithy của bạn).

    import { ServiceContext } from '../context.js';
    import { Echo as EchoOperation } from '../generated/ssdk/index.js';
    export const Echo: EchoOperation<ServiceContext> = async (input) => {
    // Logic nghiệp vụ của bạn ở đây
    return {
    message: `Echo: ${input.message}` // an toàn kiểu dựa trên mô hình Smithy của bạn
    };
    };

    Operations phải được đăng ký vào định nghĩa dịch vụ trong src/service.ts:

    import { ServiceContext } from './context.js';
    import { YourServiceService } from './generated/ssdk/index.js';
    import { Echo } from './operations/echo.js';
    // Import các operations khác ở đây
    // Đăng ký operations vào dịch vụ ở đây
    export const Service: YourServiceService<ServiceContext> = {
    Echo,
    // Thêm các operations khác ở đây
    };

    Bạn có thể định nghĩa context được chia sẻ cho các operations của bạn trong context.ts:

    export interface ServiceContext {
    // Powertools tracer, logger và metrics được cung cấp mặc định
    tracer: Tracer;
    logger: Logger;
    metrics: Metrics;
    // Thêm các dependencies được chia sẻ, kết nối database, v.v.
    dbClient: any;
    userIdentity: string;
    }

    Context này được truyền cho tất cả các triển khai operation và có thể được sử dụng để chia sẻ tài nguyên như kết nối database, cấu hình, hoặc tiện ích logging.

    Trình tạo cấu hình structured logging sử dụng AWS Lambda Powertools với tự động inject context thông qua Middy middleware.

    handler.ts
    export const handler = middy<APIGatewayProxyEvent, APIGatewayProxyResult>()
    .use(captureLambdaHandler(tracer))
    .use(injectLambdaContext(logger))
    .use(logMetrics(metrics))
    .handler(lambdaHandler);

    Bạn có thể tham chiếu logger từ các triển khai operation của bạn thông qua context:

    operations/echo.ts
    import { ServiceContext } from '../context.js';
    import { Echo as EchoOperation } from '../generated/ssdk/index.js';
    export const Echo: EchoOperation<ServiceContext> = async (input, ctx) => {
    ctx.logger.info('Thông điệp log của bạn');
    // ...
    };

    AWS X-Ray tracing được cấu hình tự động thông qua middleware captureLambdaHandler.

    handler.ts
    export const handler = middy<APIGatewayProxyEvent, APIGatewayProxyResult>()
    .use(captureLambdaHandler(tracer))
    .use(injectLambdaContext(logger))
    .use(logMetrics(metrics))
    .handler(lambdaHandler);

    Bạn có thể thêm subsegments tùy chỉnh vào traces của bạn trong các operations:

    operations/echo.ts
    import { ServiceContext } from '../context.js';
    import { Echo as EchoOperation } from '../generated/ssdk/index.js';
    export const Echo: EchoOperation<ServiceContext> = async (input, ctx) => {
    // Tạo một subsegment mới
    const subsegment = ctx.tracer.getSegment()?.addNewSubsegment('custom-operation');
    try {
    // Logic của bạn ở đây
    } catch (error) {
    subsegment?.addError(error as Error);
    throw error;
    } finally {
    subsegment?.close();
    }
    };

    CloudWatch metrics được thu thập tự động cho mỗi request thông qua middleware logMetrics.

    handler.ts
    export const handler = middy<APIGatewayProxyEvent, APIGatewayProxyResult>()
    .use(captureLambdaHandler(tracer))
    .use(injectLambdaContext(logger))
    .use(logMetrics(metrics))
    .handler(lambdaHandler);

    Bạn có thể thêm custom metrics trong các operations của bạn:

    operations/echo.ts
    import { MetricUnit } from '@aws-lambda-powertools/metrics';
    import { ServiceContext } from '../context.js';
    import { Echo as EchoOperation } from '../generated/ssdk/index.js';
    export const Echo: EchoOperation<ServiceContext> = async (input, ctx) => {
    ctx.metrics.addMetric("CustomMetric", MetricUnit.Count, 1);
    // ...
    };

    Smithy cung cấp xử lý lỗi tích hợp sẵn. Bạn có thể định nghĩa các lỗi tùy chỉnh trong mô hình Smithy của bạn:

    @error("client")
    @httpError(400)
    structure InvalidRequestError {
    @required
    message: String
    }

    Và đăng ký chúng vào operation/service của bạn:

    operation MyOperation {
    ...
    errors: [InvalidRequestError]
    }

    Sau đó ném chúng trong triển khai TypeScript của bạn:

    import { InvalidRequestError } from '../generated/ssdk/index.js';
    export const MyOperation: MyOperationHandler<ServiceContext> = async (input) => {
    if (!input.requiredField) {
    throw new InvalidRequestError({
    message: "Trường bắt buộc bị thiếu"
    });
    }
    return { /* phản hồi thành công */ };
    };

    Dự án mô hình Smithy sử dụng Docker để build các artifact Smithy và tạo TypeScript Server SDK:

    Terminal window
    pnpm nx run <model-project>:build

    Quá trình này:

    1. Biên dịch mô hình Smithy và xác thực nó
    2. Tạo đặc tả OpenAPI từ mô hình Smithy
    3. Tạo TypeScript Server SDK với các giao diện operation an toàn kiểu
    4. Xuất các build artifacts ra dist/<model-project>/build/

    Dự án backend tự động sao chép SDK được tạo trong quá trình biên dịch:

    Terminal window
    pnpm nx run <backend-project>:copy-ssdk

    Generator tự động cấu hình một target bundle sử dụng Rolldown để tạo gói triển khai:

    Terminal window
    pnpm nx run <project-name>:bundle

    Cấu hình Rolldown có thể được tìm thấy trong rolldown.config.ts, với một entry cho mỗi bundle cần tạo. Rolldown quản lý việc tạo nhiều bundle song song nếu được định nghĩa.

    Trình tạo cấu hình một server phát triển local với hot reloading:

    Terminal window
    pnpm nx run <backend-project>:serve

    Trình tạo tạo infrastructure CDK hoặc Terraform dựa trên iacProvider bạn đã chọn.

    CDK construct để triển khai API của bạn nằm trong thư mục common/constructs:

    import { MyApi } from ':my-scope/common-constructs';
    export class ExampleStack extends Stack {
    constructor(scope: Construct, id: string) {
    // Thêm API vào stack của bạn
    const api = new MyApi(this, 'MyApi', {
    integrations: MyApi.defaultIntegrations(this).build(),
    });
    }
    }

    Điều này thiết lập:

    1. Một AWS Lambda function cho dịch vụ Smithy
    2. API Gateway REST API làm trigger cho function
    3. IAM roles và permissions
    4. CloudWatch log group
    5. Cấu hình X-Ray tracing

    Các construct CDK của REST/HTTP API được cấu hình để cung cấp giao diện type-safe cho việc định nghĩa các tích hợp cho từng operation của bạn.

    Các construct CDK cung cấp hỗ trợ tích hợp type-safe đầy đủ như mô tả bên dưới.

    Bạn có thể sử dụng defaultIntegrations tĩnh để sử dụng pattern mặc định, định nghĩa một AWS Lambda function riêng biệt cho mỗi operation:

    new MyApi(this, 'MyApi', {
    integrations: MyApi.defaultIntegrations(this).build(),
    });

    Bạn có thể truy cập các AWS Lambda function bên dưới thông qua thuộc tính integrations của construct API, theo cách type-safe. Ví dụ, nếu API của bạn định nghĩa một operation tên là sayHello và bạn cần thêm một số quyền cho function này, bạn có thể làm như sau:

    const api = new MyApi(this, 'MyApi', {
    integrations: MyApi.defaultIntegrations(this).build(),
    });
    // sayHello được type theo các operation được định nghĩa trong API của bạn
    api.integrations.sayHello.handler.addToRolePolicy(new PolicyStatement({
    effect: Effect.ALLOW,
    actions: [...],
    resources: [...],
    }));

    Nếu bạn muốn tùy chỉnh các tùy chọn được sử dụng khi tạo Lambda function cho mỗi tích hợp mặc định, bạn có thể sử dụng phương thức withDefaultOptions. Ví dụ, nếu bạn muốn tất cả các Lambda function của mình nằm trong một Vpc:

    const vpc = new Vpc(this, 'Vpc', ...);
    new MyApi(this, 'MyApi', {
    integrations: MyApi.defaultIntegrations(this)
    .withDefaultOptions({
    vpc,
    })
    .build(),
    });

    Bạn cũng có thể ghi đè các tích hợp cho các operation cụ thể bằng phương thức withOverrides. Mỗi ghi đè phải chỉ định một thuộc tính integration được type theo construct tích hợp CDK phù hợp cho HTTP hoặc REST API. Phương thức withOverrides cũng là type-safe. Ví dụ, nếu bạn muốn ghi đè một API getDocumentation để trỏ đến tài liệu được lưu trữ bởi một trang web bên ngoài, bạn có thể thực hiện như sau:

    new MyApi(this, 'MyApi', {
    integrations: MyApi.defaultIntegrations(this)
    .withOverrides({
    getDocumentation: {
    integration: new HttpIntegration('https://example.com/documentation'),
    },
    })
    .build(),
    });

    Bạn cũng sẽ nhận thấy rằng tích hợp được ghi đè không còn có thuộc tính handler khi truy cập nó thông qua api.integrations.getDocumentation.

    Bạn có thể thêm các thuộc tính bổ sung vào một tích hợp cũng sẽ được type tương ứng, cho phép các loại tích hợp khác được trừu tượng hóa nhưng vẫn giữ type-safe, ví dụ nếu bạn đã tạo một tích hợp S3 cho REST API và sau này muốn tham chiếu bucket cho một operation cụ thể, bạn có thể làm như sau:

    const storageBucket = new Bucket(this, 'Bucket', { ... });
    const apiGatewayRole = new Role(this, 'ApiGatewayS3Role', {
    assumedBy: new ServicePrincipal('apigateway.amazonaws.com'),
    });
    storageBucket.grantRead(apiGatewayRole);
    const api = new MyApi(this, 'MyApi', {
    integrations: MyApi.defaultIntegrations(this)
    .withOverrides({
    getFile: {
    bucket: storageBucket,
    integration: new AwsIntegration({
    service: 's3',
    integrationHttpMethod: 'GET',
    path: `${storageBucket.bucketName}/{fileName}`,
    options: {
    credentialsRole: apiGatewayRole,
    requestParameters: {
    'integration.request.path.fileName': 'method.request.querystring.fileName',
    },
    integrationResponses: [{ statusCode: '200' }],
    },
    }),
    options: {
    requestParameters: {
    'method.request.querystring.fileName': true,
    },
    methodResponses: [{
    statusCode: '200',
    }],
    }
    },
    })
    .build(),
    });
    // Sau này, có thể trong một file khác, bạn có thể truy cập thuộc tính bucket mà chúng ta đã định nghĩa
    // theo cách type-safe
    api.integrations.getFile.bucket.grantRead(...);

    Bạn cũng có thể cung cấp options trong tích hợp của mình để ghi đè các tùy chọn method cụ thể như authorizer, ví dụ nếu bạn muốn sử dụng xác thực Cognito cho operation getDocumentation của mình:

    new MyApi(this, 'MyApi', {
    integrations: MyApi.defaultIntegrations(this)
    .withOverrides({
    getDocumentation: {
    integration: new HttpIntegration('https://example.com/documentation'),
    options: {
    authorizer: new CognitoUserPoolsAuthorizer(...) // cho REST, hoặc HttpUserPoolAuthorizer cho HTTP API
    }
    },
    })
    .build(),
    });

    Nếu bạn muốn, bạn có thể chọn không sử dụng các tích hợp mặc định và thay vào đó cung cấp trực tiếp một tích hợp cho mỗi operation. Điều này hữu ích nếu, ví dụ, mỗi operation cần sử dụng một loại tích hợp khác nhau hoặc bạn muốn nhận lỗi type khi thêm các operation mới:

    new MyApi(this, 'MyApi', {
    integrations: {
    sayHello: {
    integration: new LambdaIntegration(...),
    },
    getDocumentation: {
    integration: new HttpIntegration(...),
    },
    },
    });

    Nếu bạn muốn triển khai một Lambda function duy nhất để phục vụ tất cả các request API, bạn có thể tự do chỉnh sửa phương thức defaultIntegrations cho API của mình để tạo một function duy nhất thay vì một function cho mỗi tích hợp:

    packages/common/constructs/src/app/apis/my-api.ts
    export class MyApi<...> extends ... {
    public static defaultIntegrations = (scope: Construct) => {
    const router = new Function(scope, 'RouterHandler', { ... });
    return IntegrationBuilder.rest({
    ...
    defaultIntegrationOptions: {},
    buildDefaultIntegration: (op) => {
    return {
    // Tham chiếu cùng một router lambda handler trong mọi tích hợp
    integration: new LambdaIntegration(router),
    };
    },
    });
    };
    }

    Bạn có thể sửa đổi code theo các cách khác nếu muốn, ví dụ bạn có thể muốn định nghĩa function router như một tham số cho defaultIntegrations thay vì xây dựng nó trong phương thức.

    Vì operations được định nghĩa trong Smithy, chúng tôi sử dụng tạo mã để cung cấp metadata cho CDK construct để có integrations an toàn kiểu.

    Một target generate:<ApiName>-metadata được thêm vào project.json của common constructs để tạo điều kiện cho việc tạo mã này, xuất một file như packages/common/constructs/src/generated/my-api/metadata.gen.ts. Vì điều này được tạo tại thời điểm build, nó bị bỏ qua trong kiểm soát phiên bản.

    Nếu bạn đã chọn xác thực IAM, bạn có thể sử dụng phương thức grantInvokeAccess để cấp quyền truy cập vào API của bạn:

    api.grantInvokeAccess(myIdentityPool.authenticatedRole);

    Để gọi API của bạn từ một React website, bạn có thể sử dụng trình tạo api-connection, cung cấp tạo client an toàn kiểu từ mô hình Smithy của bạn.