跳转到内容

Smithy TypeScript API

Filter this guidePick generator option values to hide sections that don't apply.

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#api
  5. 填写必需参数
    • framework: smithy
  6. 点击 Generate
参数类型默认值描述
name 必需string-API 的名称(必填)。用于生成类名和文件路径。
framework trpc | smithytrpc要使用的API框架。
namespace string-Smithy API 的命名空间(仅适用于 smithy 框架)。默认为您的 monorepo 作用域
integrationPattern isolated | sharedisolatedAPI Gateway 集成的生成方式。可选择 isolated(默认)或 shared。
auth iam | cognito | customiam用于对 API 进行身份验证的方法。可选择 iam(默认)、cognito 或 custom。
directory stringpackages存储应用程序的目录。
subDirectory string-项目所在的子目录。默认情况下为项目名称。
iac inherit | cdk | terraforminherit首选的 IaC 提供商。默认情况下,这将继承您的初始选择。
infra rest-lambda | http-lambda | nonerest-lambda用于部署此 API 的基础设施类型。
preferInstallDependencies booleantrue是否在生成器运行后优先安装依赖项。设置为 false 可在批量运行多个生成器时延迟安装(如果后续生成器需要计算 Nx 项目图,仍会运行安装);在最后统一安装一次。

生成器在 <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 API 具有以下架构,在 API Gateway 阶段前面有一个 AWS WAFv2 Web ACL:

ClientWAFAPI Gateway(REST API)Lambda(Smithy Server SDK)CloudWatch(Logs, Metrics)X-Ray(Traces)

操作在模型项目的 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 { /* 成功响应 */ };
};

当您的 API 受身份验证保护时,您的操作通常需要知道是谁在调用。推荐的方法是在处理程序中一次性解析调用者的身份,并通过服务上下文传递它,以供特定操作使用。

我们将未授权的情况建模为 Smithy 错误,以便将其序列化为正确的 403 响应。将其添加到您的模型中,例如在 model/src/operations/errors.smithy 中,并在任何需要身份的操作上引用它:

$version: "2.0"
namespace your.namespace
/// Thrown when the calling user cannot be determined
@error("client")
@httpError(403)
structure UnauthorizedError {
@required
message: String
}

首先,在 src/context.ts 中的服务上下文上公开解析后的身份。我们将其提供为一个函数,以便 UnauthorizedError 从操作内部抛出(Server SDK 将其序列化为 403),而不是从处理程序抛出:

import { Logger } from '@aws-lambda-powertools/logger';
import { Metrics } from '@aws-lambda-powertools/metrics';
import { Tracer } from '@aws-lambda-powertools/tracer';
export interface Identity {
sub: string;
username: string;
}
/**
* Context provided to all operations.
*/
export interface ServiceContext {
tracer: Tracer;
logger: Logger;
metrics: Metrics;
getIdentity: () => Promise<Identity>;
}

接下来,在 src/identity.ts 中编写解析器。当无法确定调用者时,它会抛出 UnauthorizedError。实现取决于您选择的 auth 方法:

auth = iam

对于 IAM 身份验证,我们使用从 API Gateway 事件中提取的 sub 在 Cognito 中查找调用者:

import { CognitoIdentityProvider } from '@aws-sdk/client-cognito-identity-provider';
import type { APIGatewayProxyEvent } from 'aws-lambda';
import { Identity } from './context.js';
import { UnauthorizedError } from './generated/ssdk/index.js';
const cognito = new CognitoIdentityProvider();
export const getIdentity = async (
event: APIGatewayProxyEvent,
): Promise<Identity> => {
const cognitoAuthenticationProvider =
event.requestContext?.identity?.cognitoAuthenticationProvider;
let sub: string | undefined = undefined;
if (cognitoAuthenticationProvider) {
const providerParts = cognitoAuthenticationProvider.split(':');
sub = providerParts[providerParts.length - 1];
}
if (!sub) {
throw new UnauthorizedError({ message: 'Unable to determine calling user' });
}
const { Users } = await cognito.listUsers({
// Assumes user pool id is configured in lambda environment
UserPoolId: process.env.USER_POOL_ID!,
Limit: 1,
Filter: `sub="${sub}"`,
});
if (!Users || Users.length !== 1) {
throw new UnauthorizedError({ message: `No user found with subjectId ${sub}` });
}
return { sub, username: Users[0].Username! };
};
auth = cognito

使用 auth: 'cognito' 时,API Gateway Cognito 用户池授权器会验证调用者在 Authorization 标头中提供的 JWT,并将验证后的声明放置在事件的 event.requestContext.authorizer.claims 上:

import type { APIGatewayProxyEvent } from 'aws-lambda';
import { Identity } from './context.js';
import { UnauthorizedError } from './generated/ssdk/index.js';
export const getIdentity = async (
event: APIGatewayProxyEvent,
): Promise<Identity> => {
const claims = event.requestContext?.authorizer?.claims as
| Record<string, string>
| undefined;
const sub = claims?.sub;
const username = claims?.username;
if (!sub || !username) {
throw new UnauthorizedError({ message: 'Unable to determine calling user' });
}
return { sub, username };
};

然后在 src/handler.ts 中将解析器连接到上下文:

import { Service } from './service.js';
import { getIdentity } from './identity.js';
// ...
const httpResponse = await serviceHandler.handle(httpRequest, {
tracer,
logger,
metrics,
getIdentity: () => getIdentity(event),
});

现在我们可以在操作中使用解析后的身份,例如在 src/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 identity = await ctx.getIdentity();
return { message: `${identity.username} says ${input.message}` };
};

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 追踪配置
auth = cognito

对于 REST API,生成的构造默认会将 AWS WAFv2 Web ACL 与 API Gateway 阶段关联。Web ACL 使用 AWS 托管的默认规则集(AWSManagedRulesCommonRuleSetAWSManagedRulesKnownBadInputsRuleSet),提供针对常见 Web 漏洞(包括 OWASP Top 10)的保护。WAF 请求日志会写入 CloudWatch Logs 日志组。

您可以编辑生成的 rest-api 构造来添加、删除或调整规则(例如,添加基于速率的规则或其他托管规则组)。

要选择退出(例如,附加您自己的 Web ACL),请将 enableWaf 设置为 false

const api = new MyApi(this, 'MyApi', {
integrations: MyApi.defaultIntegrations(this).build(),
enableWaf: false,
});

对于 REST API,生成的基础设施默认启用访问日志,为每个请求向专用的 CloudWatch Logs 组写入一行结构化的 JSON 数据。日志组使用客户管理的 KMS 密钥进行加密,并保留一年。

API Gateway 使用账户级别的 CloudWatch Logs 角色来写入访问日志。此角色配置在 AWS::ApiGateway::Account 设置上,该设置是每个区域每个账户的单例 — 区域中的每个 REST API 只有一个角色。为了在多个独立部署的堆栈中安全地管理此角色,生成的基础设施:

  • 仅在尚未设置工作角色时创建共享的 CloudWatch Logs 角色并在账户上配置它,因此部署永远不会覆盖另一个堆栈拥有的角色。
  • 在拆除时保持账户设置不变,因此销毁一个堆栈永远不会禁用区域中其他 REST API 的日志记录。

账户角色由 ApiGatewayAccount 构造管理,这是一个通过 ApiGatewayAccount.ensure(scope) 解析的堆栈作用域单例。每个 REST API 的阶段都依赖于它,该角色由 Lambda 支持的自定义资源配置。

您可以通过在构造 API 时传递 deployOptions 来自定义访问日志格式:

const api = new MyApi(this, 'MyApi', {
integrations: MyApi.defaultIntegrations(this).build(),
deployOptions: {
accessLogFormat: AccessLogFormat.clf(),
},
});

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(),
});

要为_特定_操作自定义创建默认集成时使用的选项(而不影响其他操作),可以使用 withOperationOptions 方法。例如,如果您只想增加一个操作的 Lambda 函数超时时间:

const api = new MyApi(this, 'MyApi', {
integrations: MyApi.defaultIntegrations(this)
.withOperationOptions({
sayHello: {
timeout: Duration.seconds(60),
},
})
.build(),
});
// 选定的操作仍然是默认集成,因此它们仍然具有相应的类型:
api.integrations.sayHello.handler.addToRolePolicy(new PolicyStatement({ ... }));

您指定的选项会与默认集成选项(以及通过 withDefaultOptions 设置的任何选项)合并。请注意,您不能为通过 withOverrides 替换的操作指定选项,因为这些操作不再使用默认集成。

如果同一操作同时被 withOperationOptionswithOverrides 指定,无论调用顺序如何,都会遇到类型错误。

使用 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 的文件。由于此文件在构建时生成,版本控制中会忽略。

auth = iam

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

api.grantInvokeAccess(myIdentityPool.authenticatedRole);

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

使用 connection 生成器将此项目与工作区中的其他项目集成。以下连接涉及此项目:

Smithy
React to Smithy API从 React 网站调用 Smithy API
SmithyAmazon Aurora
Smithy API to Relational Database将 Smithy API 连接到 Aurora 关系数据库
SmithyAmazon DynamoDB
Smithy API to TypeScript DynamoDB将 Smithy API 连接到 DynamoDB 表