跳转到内容

Smithy TypeScript API

Smithy 是一个协议无关的接口定义语言,用于以模型驱动的方式编写 API。

Smithy TypeScript API 生成器通过 Smithy 进行服务定义,并基于 Smithy TypeScript Server SDK 实现创建新的 API。该生成器提供 CDK 或 Terraform 基础设施即代码,用于将服务部署到 AWS Lambda 并通过 AWS API Gateway REST API 暴露。它支持基于 Smithy 模型的自动代码生成,实现类型安全的 API 开发。生成的处理器使用 AWS Lambda Powertools for TypeScript 实现可观测性,包括日志记录、AWS X-Ray 追踪和 CloudWatch 指标。

可通过两种方式生成新的 Smithy TypeScript API:

  1. 安装 Nx Console VSCode Plugin 如果您尚未安装
  2. 在VSCode中打开Nx控制台
  3. 点击 Generate (UI) 在"Common Nx Commands"部分
  4. 搜索 @aws/nx-plugin - ts#smithy-api
  5. 填写必需参数
    • 点击 Generate
    参数 类型 默认值 描述
    name 必需 string - API 的名称(必需)。用于生成类名和文件路径。
    namespace string - Smithy API 的命名空间。默认为您的 monorepo 作用域
    computeType string ServerlessApiGatewayRestApi 用于部署此 API 的计算类型。
    integrationPattern string isolated 为 API 生成 API Gateway 集成的方式。可选择 isolated(默认)或 shared。
    auth string IAM 用于对 API 进行身份验证的方法。可选择 IAM(默认)、Cognito 或 None。
    directory string packages 存储应用程序的目录。
    subDirectory string - 项目所在的子目录。默认情况下为项目名称。
    iacProvider string Inherit 首选的 IaC 提供商。默认情况下,这继承自您的初始选择。

    生成器在 <directory>/<api-name> 目录下创建两个关联项目:

    • 文件夹model/ Smithy 模型项目
      • project.json 项目配置与构建目标
      • smithy-build.json Smithy 构建配置
      • build.Dockerfile 构建 Smithy 产物的 Docker 配置
      • 文件夹src/
        • main.smithy 主服务定义
        • 文件夹operations/
          • echo.smithy 示例操作定义
    • 文件夹backend/ TypeScript 后端实现
      • project.json 项目配置与构建目标
      • rolldown.config.ts 打包配置
      • 文件夹src/
        • handler.ts AWS Lambda 处理器
        • local-server.ts 本地开发服务器
        • service.ts 服务实现
        • context.ts 服务上下文定义
        • 文件夹operations/
          • echo.ts 示例操作实现
        • 文件夹generated/ 生成的 TypeScript SDK(构建时创建)

    由于此生成器根据所选 iacProvider 创建基础设施即代码,因此会在 packages/common 中创建包含相关 CDK 构造或 Terraform 模块的项目。

    通用基础设施即代码项目结构如下:

    • 文件夹packages/common/constructs
      • 文件夹src
        • 文件夹app/ 针对特定项目/生成器的基础设施构造
          • 文件夹apis/
            • <project-name>.ts 部署 API 的 CDK 构造
        • 文件夹core/ app 中构造复用的通用构造
          • 文件夹api/
            • rest-api.ts 部署 REST API 的 CDK 构造
            • utils.ts API 构造的实用工具
        • index.ts 导出 app 构造的入口点
      • project.json 项目构建目标与配置

    操作在模型项目的 Smithy 文件中定义。主服务定义位于 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,
    // 在此添加操作
    ]
    errors: [
    ValidationException
    ]
    }

    独立操作在 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
    }

    操作实现位于后端项目的 src/operations/ 目录。每个操作使用 TypeScript Server SDK(根据 Smithy 模型在构建时生成)生成的类型实现。

    import { ServiceContext } from '../context.js';
    import { Echo as EchoOperation } from '../generated/ssdk/index.js';
    export const Echo: EchoOperation<ServiceContext> = async (input) => {
    // 业务逻辑在此编写
    return {
    message: `Echo: ${input.message}` // 基于 Smithy 模型的类型安全
    };
    };

    操作需在 src/service.ts 中注册到服务定义:

    import { ServiceContext } from './context.js';
    import { YourServiceService } from './generated/ssdk/index.js';
    import { Echo } from './operations/echo.js';
    // 导入其他操作
    // 在此将操作注册到服务
    export const Service: YourServiceService<ServiceContext> = {
    Echo,
    // 添加其他操作
    };

    可在 context.ts 中定义操作的共享上下文:

    export interface ServiceContext {
    // 默认提供 Powertools 追踪器、日志记录器和指标
    tracer: Tracer;
    logger: Logger;
    metrics: Metrics;
    // 添加共享依赖项、数据库连接等
    dbClient: any;
    userIdentity: string;
    }

    此上下文传递给所有操作实现,可用于共享数据库连接、配置或日志工具等资源。

    使用 AWS Lambda Powertools 实现可观测性

    Section titled “使用 AWS Lambda Powertools 实现可观测性”

    生成器通过 Middy 中间件配置结构化日志记录,并自动注入上下文。

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

    可通过上下文在操作实现中引用日志记录器:

    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('您的日志消息');
    // ...
    };

    通过 captureLambdaHandler 中间件自动配置 AWS X-Ray 追踪。

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

    可在操作中添加自定义子段到追踪中:

    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) => {
    // 创建新子段
    const subsegment = ctx.tracer.getSegment()?.addNewSubsegment('custom-operation');
    try {
    // 业务逻辑在此编写
    } catch (error) {
    subsegment?.addError(error as Error);
    throw error;
    } finally {
    subsegment?.close();
    }
    };

    通过 logMetrics 中间件自动收集每个请求的 CloudWatch 指标。

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

    可在操作中添加自定义指标:

    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 提供内置错误处理。可在 Smithy 模型中定义自定义错误:

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

    并将其注册到操作/服务:

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

    然后在 TypeScript 实现中抛出:

    import { InvalidRequestError } from '../generated/ssdk/index.js';
    export const MyOperation: MyOperationHandler<ServiceContext> = async (input) => {
    if (!input.requiredField) {
    throw new InvalidRequestError({
    message: "必填字段缺失"
    });
    }
    return { /* 成功响应 */ };
    };

    Smithy 模型项目使用 Docker 构建 Smithy 产物并生成 TypeScript Server SDK:

    Terminal window
    pnpm nx build <model-project>

    此过程:

    1. 编译 Smithy 模型 并进行验证
    2. 生成 OpenAPI 规范 从 Smithy 模型
    3. 创建 TypeScript Server SDK 包含类型安全的操作接口
    4. 输出构建产物dist/<model-project>/build/

    后端项目在编译时自动复制生成的 SDK:

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

    生成器会自动配置一个使用 Rolldown 创建部署包的 bundle 目标:

    Terminal window
    pnpm nx bundle <project-name>

    Rolldown 配置位于 rolldown.config.ts 文件中,每个要生成的包都有对应的入口配置。如果定义了多个包,Rolldown 会并行管理这些包的创建过程。

    生成器配置了支持热重载的本地开发服务器:

    Terminal window
    pnpm nx serve <backend-project>

    生成器根据所选 iacProvider 创建 CDK 或 Terraform 基础设施。

    部署 API 的 CDK 构造位于 common/constructs 文件夹:

    import { MyApi } from ':my-scope/common-constructs';
    export class ExampleStack extends Stack {
    constructor(scope: Construct, id: string) {
    // 将 API 添加到堆栈
    const api = new MyApi(this, 'MyApi', {
    integrations: MyApi.defaultIntegrations(this).build(),
    });
    }
    }

    此配置包括:

    1. 用于 Smithy 服务的 AWS Lambda 函数
    2. 作为函数触发器的 API Gateway REST API
    3. IAM 角色与权限
    4. CloudWatch 日志组
    5. X-Ray 追踪配置

    REST/HTTP API 的 CDK 构造被配置为提供类型安全接口,用于为每个操作定义集成。

    CDK 构造提供如下所述完整的类型安全集成支持。

    您可以使用静态方法 defaultIntegrations 来应用默认模式,这会为每个操作定义独立的 AWS Lambda 函数:

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

    您可以通过 API 构造的 integrations 属性以类型安全的方式访问底层 AWS Lambda 函数。例如,如果您的 API 定义了名为 sayHello 的操作,并需要为此函数添加权限:

    const api = new MyApi(this, 'MyApi', {
    integrations: MyApi.defaultIntegrations(this).build(),
    });
    // sayHello 的类型与 API 中定义的操作相匹配
    api.integrations.sayHello.handler.addToRolePolicy(new PolicyStatement({
    effect: Effect.ALLOW,
    actions: [...],
    resources: [...],
    }));

    如果您的 API 使用 shared 模式,共享路由 Lambda 函数通过 api.integrations.$router 暴露:

    const api = new MyApi(this, 'MyApi', {
    integrations: MyApi.defaultIntegrations(this).build(),
    });
    api.integrations.$router.handler.addEnvironment('LOG_LEVEL', 'DEBUG');

    如需自定义创建 Lambda 函数时的默认选项,可使用 withDefaultOptions 方法。例如为所有 Lambda 函数配置 VPC:

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

    使用 withOverrides 方法可以覆盖特定操作的集成。每个覆盖必须指定 integration 属性,其类型对应 HTTP 或 REST API 的相应 CDK 集成构造。withOverrides 方法也是类型安全的。例如将 getDocumentation API 指向外部网站托管的文档:

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

    覆盖后的集成通过 api.integrations.getDocumentation 访问时将不再具有 handler 属性。

    您可以为集成添加额外属性以实现类型安全的抽象。例如为 REST API 创建 S3 集成后引用存储桶:

    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(),
    });
    // 后续可类型安全地访问我们定义的 bucket 属性
    api.integrations.getFile.bucket.grantRead(...);

    可在集成中提供 options 来覆盖特定方法选项,例如为 getDocumentation 操作使用 Cognito 认证:

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

    您可以选择不使用默认集成,直接为每个操作提供集成。这在需要不同集成类型或希望在添加新操作时收到类型错误提示时非常有用:

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

    生成的 CDK API 构造支持两种集成模式:

    • isolated 为每个操作创建一个 Lambda 函数。这是生成 API 的默认模式。
    • shared 创建单个默认路由 Lambda 函数并在所有操作中复用,除非您覆盖特定集成。

    isolated 为每个操作提供更细粒度的权限和配置。shared 减少 Lambda 和 API Gateway 集成的数量,同时仍允许选择性覆盖。

    例如,将 pattern 设置为 'shared' 会创建单个函数而不是为每个集成创建一个:

    packages/common/constructs/src/app/apis/my-api.ts
    export class MyApi<...> extends ... {
    public static defaultIntegrations = (scope: Construct) => {
    ...
    return IntegrationBuilder.rest({
    pattern: 'shared',
    ...
    });
    };
    }

    由于操作在 Smithy 中定义,我们使用代码生成向 CDK 构造提供元数据以实现类型安全集成。

    在通用构造的 project.json 中添加 generate:<ApiName>-metadata 目标以促进此代码生成,生成如 packages/common/constructs/src/generated/my-api/metadata.gen.ts 的文件。由于此文件在构建时生成,版本控制中会忽略。

    如果选择 IAM 认证,可使用 grantInvokeAccess 方法授予 API 访问权限:

    api.grantInvokeAccess(myIdentityPool.authenticatedRole);

    要从 React 网站调用 API,可使用 connection 生成器,该生成器根据 Smithy 模型提供类型安全的客户端生成。