Skip to content

Smithy TypeScriptのAPI

Smithyはプロトコルに依存しないインターフェース定義言語で、モデル駆動のアプローチでAPIを設計できます。

Smithy TypeScript APIジェネレータは、サービス定義にSmithyを、実装にSmithy TypeScript Server SDKを使用した新しいAPIを作成します。このジェネレータはCDKまたはTerraformのInfrastructure as Codeを提供し、AWS LambdaにデプロイされたサービスをAWS API Gateway REST API経由で公開します。Smithyモデルからの自動コード生成により、型安全なAPI開発を実現します。生成されたハンドラはAWS Lambda Powertools for TypeScriptを使用し、ロギング、AWS X-Rayトレーシング、CloudWatchメトリクスなどの観測可能性を備えています。

新しいSmithy TypeScript APIは2つの方法で生成できます:

  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 - 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.

    ジェネレータは<directory>/<api-name>ディレクトリに2つの関連プロジェクトを作成します:

    • Directorymodel/ Smithyモデルプロジェクト
      • project.json プロジェクト設定とビルドターゲット
      • smithy-build.json Smithyビルド設定
      • build.Dockerfile Smithyアーティファクトビルド用Docker設定
      • Directorysrc/
        • main.smithy メインサービス定義
        • Directoryoperations/
          • echo.smithy オペレーション定義例
    • Directorybackend/ TypeScriptバックエンド実装
      • project.json プロジェクト設定とビルドターゲット
      • rolldown.config.ts バンドル設定
      • Directorysrc/
        • handler.ts AWS Lambdaハンドラ
        • local-server.ts ローカル開発サーバ
        • service.ts サービス実装
        • context.ts サービスコンテキスト定義
        • Directoryoperations/
          • echo.ts オペレーション実装例
        • Directorygenerated/ 生成されたTypeScript SDK(ビルド時に作成)

    このジェネレータは選択したiacProviderに基づいたInfrastructure as Codeを作成するため、packages/commonにCDKコンストラクトまたはTerraformモジュールを含むプロジェクトを生成します。

    共通のInfrastructure as Codeプロジェクトの構造は以下の通りです:

    • Directorypackages/common/constructs
      • Directorysrc
        • Directoryapp/ プロジェクト/ジェネレータ固有のインフラストラクチャ用コンストラクト
          • Directoryapis/
            • <project-name>.ts APIデプロイ用CDKコンストラクト
        • Directorycore/ 再利用可能な汎用コンストラクト
          • Directoryapi/
            • rest-api.ts REST APIデプロイ用コンストラクト
            • utils.ts APIコンストラクト用ユーティリティ
        • index.ts コンストラクトエクスポート用エントリポイント
      • 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
    }

    TypeScriptでのオペレーション実装

    Section titled “TypeScriptでのオペレーション実装”

    オペレーション実装はバックエンドプロジェクトのsrc/operations/ディレクトリに配置されます。各オペレーションはSmithyモデルからビルド時に生成されるTypeScript Server SDKの型を使用して実装されます。

    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、Logger、Metricsがデフォルトで提供
    tracer: Tracer;
    logger: Logger;
    metrics: Metrics;
    // 共有依存関係、データベース接続などを追加
    dbClient: any;
    userIdentity: string;
    }

    このコンテキストはすべてのオペレーション実装に渡され、データベース接続や設定、ロギングユーティリティなどのリソース共有に使用できます。

    AWS Lambda Powertoolsによる観測可能性

    Section titled “AWS Lambda Powertoolsによる観測可能性”

    構造化ロギングはMiddyミドルウェア経由で自動コンテキスト注入されるAWS Lambda Powertoolsで設定されます。

    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('ログメッセージ');
    // ...
    };

    AWS X-RayトレーシングはcaptureLambdaHandlerミドルウェアで自動設定されます。

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

    CloudWatchメトリクスはlogMetricsミドルウェアで自動収集されます。

    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 run <model-project>:build

    このプロセスでは:

    1. Smithyモデルのコンパイルと検証
    2. SmithyモデルからOpenAPI仕様の生成
    3. 型安全なオペレーションインターフェースを含むTypeScript Server SDKの作成
    4. ビルドアーティファクトをdist/<model-project>/build/に出力

    バックエンドプロジェクトはビルド時に生成されたSDKを自動コピーします:

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

    ジェネレーターは自動的に Rolldown を使用する bundle ターゲットを設定します。このターゲットはデプロイメントパッケージの作成に使用されます:

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

    Rolldownの設定はrolldown.config.tsに記述され、生成するバンドルごとにエントリを定義します。Rolldownは定義された複数のバンドルを並行して作成する処理を管理します。

    ホットリロード機能を備えたローカル開発サーバが設定されます:

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

    ジェネレータは選択した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: [...],
    }));

    デフォルトオプションのカスタマイズ

    Section titled “デフォルトオプションのカスタマイズ”

    デフォルト統合で作成されるLambda関数のオプションをカスタマイズするには、withDefaultOptionsメソッドを使用します。例えば、すべてのLambda関数をVPC内に配置する場合:

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

    withOverridesメソッドを使用して特定のオペレーションの統合をオーバーライドできます。各オーバーライドは、HTTPまたはREST APIに適したCDK統合コンストラクトに型付けされたintegrationプロパティを指定する必要があります。例として、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(...);

    オーソライザーのオーバーライド

    Section titled “オーソライザーのオーバーライド”

    統合に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用、HTTP APIの場合はHttpUserPoolAuthorizer
    }
    },
    })
    .build(),
    });

    必要に応じて、デフォルト統合を使用せずに各オペレーションに直接統合を指定できます。例えば、オペレーションごとに異なる統合タイプを使用する場合や、新しいオペレーション追加時に型エラーを受け取りたい場合に有用です:

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

    すべてのAPIリクエストを処理する単一のLambda関数をデプロイしたい場合、APIのdefaultIntegrationsメソッドを編集して、統合ごとではなく単一の関数を作成できます:

    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 {
    // すべての統合で同じルータLambdaハンドラを参照
    integration: new LambdaIntegration(router),
    };
    },
    });
    };
    }

    必要に応じて、router関数をdefaultIntegrationsのパラメータとして定義するなど、他の方法でコードを修正することもできます。

    Smithyで定義されたオペレーションのメタデータをCDKコンストラクトに提供するため、コード生成を使用します。

    共通コンストラクトのproject.jsongenerate:<ApiName>-metadataターゲットが追加され、packages/common/constructs/src/generated/my-api/metadata.gen.tsのようなファイルが生成されます。これはビルド時に生成されるため、バージョン管理から除外されます。

    IAM認証を選択した場合、grantInvokeAccessメソッドでAPIへのアクセス権限を付与できます:

    api.grantInvokeAccess(myIdentityPool.authenticatedRole);

    ReactウェブサイトからAPIを呼び出すには、api-connectionジェネレータを使用できます。これはSmithyモデルから型安全なクライアントを生成します。