콘텐츠로 이동

Smithy TypeScript API

Smithy는 프로토콜에 구애받지 않는 모델 기반 API 정의 언어입니다.

Smithy TypeScript API 생성기는 Smithy를 서비스 정의에 사용하고 Smithy TypeScript Server SDK를 구현에 활용하는 새로운 API를 생성합니다. 이 생성기는 AWS Lambda에 서비스를 배포하기 위한 CDK 또는 Terraform 인프라스트럭처 코드를 제공하며, AWS API Gateway REST API를 통해 노출됩니다. Smithy 모델에서 자동 코드 생성을 통해 타입 안전한 API 개발을 지원합니다. 생성된 핸들러는 로깅, AWS X-Ray 트레이싱, CloudWatch 메트릭을 포함한 관측성을 위해 AWS Lambda Powertools for TypeScript를 사용합니다.

새로운 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 - 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> 디렉토리에 두 개의 관련 프로젝트를 생성합니다:

    • 디렉터리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
    }

    TypeScript에서 오퍼레이션 구현하기

    섹션 제목: “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: Tracer;
    logger: Logger;
    metrics: Metrics;
    // 공유 의존성, DB 연결 등 추가
    dbClient: any;
    userIdentity: string;
    }

    이 컨텍스트는 모든 오퍼레이션 구현에 전달되며 데이터베이스 연결, 설정, 로깅 유틸리티 등을 공유하는 데 사용됩니다.

    생성기는 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 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: [...],
    }));

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

    통합에 options를 제공하여 Cognito 인증과 같은 특정 메서드 옵션을 재정의할 수 있습니다. 예를 들어 getDocumentation 작업에 Cognito 인증을 사용하려면:

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

    기본 통합 대신 각 작업에 직접 통합을 제공할 수 있습니다. 이는 각 작업이 다른 유형의 통합을 사용해야 하거나 새 작업 추가 시 타입 오류를 수신하려는 경우 유용합니다:

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

    모든 API 요청을 처리하는 단일 Lambda 함수를 배포하려면 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 모델에서 타입 안전 클라이언트를 생성할 수 있습니다.