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 - APIの名前(必須)。クラス名とファイルパスの生成に使用されます。
    namespace string - Smithy APIの名前空間。デフォルトではモノレポのスコープが使用されます
    computeType string ServerlessApiGatewayRestApi このAPIをデプロイするために使用するコンピュートのタイプ
    integrationPattern string isolated API GatewayインテグレーションがAPIに対してどのように生成されるか。isolated(デフォルト)とsharedから選択します。
    auth string IAM APIへの認証に使用される方法。IAM(デフォルト)、Cognito、Noneから選択します。
    directory string packages アプリケーションを保存するディレクトリ
    subDirectory string - プロジェクトが配置されるサブディレクトリ。デフォルトではプロジェクト名になります。
    iacProvider string Inherit 優先されるIaCプロバイダー。デフォルトでは初期選択から継承されます。

    ジェネレータは<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 build <model-project>

    このプロセスでは:

    1. Smithyモデルのコンパイルと検証
    2. SmithyモデルからのOpenAPI仕様の生成
    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');

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

    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プロパティを指定する必要があります。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(...);

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

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

    生成されたCDK APIコンストラクトは2つの統合パターンをサポートしています:

    • isolatedはオペレーションごとに1つのLambda関数を作成します。これが生成されたAPIのデフォルトです。
    • sharedは単一のデフォルトルーターLambdaを作成し、特定の統合をオーバーライドしない限りすべてのオペレーションで再利用します。

    isolatedはオペレーションごとにきめ細かい権限と設定を提供します。sharedはLambdaとAPI Gateway統合の増殖を抑えつつ、選択的なオーバーライドを可能にします。

    例えば、pattern'shared'に設定すると、統合ごとに1つではなく単一の関数を作成します:

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

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

    api.grantInvokeAccess(myIdentityPool.authenticatedRole);

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